diff --git a/.cursorignore b/.cursorignore
deleted file mode 100644
index db9dd609e9..0000000000
--- a/.cursorignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
-spm-files
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7a21ba6ee2..e7f834374d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,33 @@
-fastlane/README.md
-fastlane/report.xml
-fastlane/test_output/*
-fastlane/fastlanematch
-build/*
+# WinterGram — files that should never be committed
+
+# -----------------------------------------------------------------------------
+# Build outputs and caches
+# -----------------------------------------------------------------------------
+build/
+build-input/
+bazel-bin
+bazel-bin/*
+bazel-genfiles
+bazel-genfiles/*
+bazel-out
+bazel-out/*
+bazel-testlogs
+bazel-testlogs/*
+bazel-telegram-ios
+bazel-telegram-ios/*
+bazel-WinterGram
+bazel-WinterGram/*
+buck-out/*
+.buckd/*
+DerivedData
+*.hmap
+*.dSYM
+*.dSYM.zip
+*.ipa
+
+# -----------------------------------------------------------------------------
+# Xcode / IDE user state
+# -----------------------------------------------------------------------------
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -15,28 +40,52 @@ xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
-DerivedData
-*.hmap
-*.ipa
*.xcuserstate
-.DS_Store
-*.dSYM
-*.dSYM.zip
-*.ipa
*/xcuserdata/*
-buck-out/*
-.buckd/*
-tools/buck
-tools/bazel
-AppBinary.xcworkspace/*
-Project.xcodeproj/*
-Watch/Watch.xcodeproj/*
-AppBundle.xcworkspace/*
*.xcworkspace
*.xcodeproj
!*_Xcode.xcodeproj
.idea
*/.idea/*
+
+# -----------------------------------------------------------------------------
+# WinterGram local configuration
+# -----------------------------------------------------------------------------
+build-system/wintergram-development-configuration.json
+build-system/fake-codesigning-wintergram/
+
+# -----------------------------------------------------------------------------
+# Editor / AI assistant / local tooling
+# -----------------------------------------------------------------------------
+.vscode/
+.claude/
+**/.claude/settings.local.json
+.xcodebuildmcp/
+tools/buck
+tools/bazel
+
+# -----------------------------------------------------------------------------
+# OS and swap files
+# -----------------------------------------------------------------------------
+.DS_Store
+*.swp
+*/*.swp
+
+# -----------------------------------------------------------------------------
+# Python / Node artifacts
+# -----------------------------------------------------------------------------
+**/*.pyc
+*.pyc
+**/node_modules/
+tools/sim-watcher/dist/
+
+# -----------------------------------------------------------------------------
+# Submodule-generated headers and prebuilt binaries
+# -----------------------------------------------------------------------------
+submodules/OpusBinding/SharedHeaders/*
+submodules/FFMpegBinding/SharedHeaders/*
+submodules/OpenSSLEncryptionProvider/SharedHeaders/*
+submodules/TelegramCore/FlatSerialization/Sources/*
submodules/MtProtoKit/TON/macOS/lib/libtonlibjson.0.5.dylib
submodules/MtProtoKit/TON/macOS/lib/libtonlibjson.dylib
submodules/MtProtoKit/TON/macOS/lib/cmake/Tonlib/TonlibConfig.cmake
@@ -49,37 +98,26 @@ submodules/MtProtoKit/TON/macOS/lib/fift/Lisp.fif
submodules/MtProtoKit/TON/macOS/lib/fift/Lists.fif
submodules/MtProtoKit/TON/macOS/lib/fift/Stack.fif
submodules/MtProtoKit/TON/macOS/lib/fift/TonUtil.fif
-bazel-bin
-bazel-bin/*
-bazel-genfiles
-bazel-genfiles/*
-bazel-out
-bazel-out/*
-bazel-telegram-ios
-bazel-telegram-ios/*
-bazel-testlogs
-bazel-testlogs/*
+
+# -----------------------------------------------------------------------------
+# Bazel / SPM / LSP generated files
+# -----------------------------------------------------------------------------
xcodeproj.bazelrc
-*/*.swp
-*.swp
-build-input/*
-**/*.pyc
-*.pyc
-submodules/OpusBinding/SharedHeaders/*
-submodules/FFMpegBinding/SharedHeaders/*
-submodules/OpenSSLEncryptionProvider/SharedHeaders/*
-submodules/TelegramCore/FlatSerialization/Sources/*
buildServer.json
.build/**
-Telegram.LSP.json
**/.build/**
+Telegram.LSP.json
spm-files
xcode-files
.bsp/**
.sourcekit-lsp/**
-/.claude/
-**/.claude/settings.local.json
-**/.vscode/launch.json
-/buildbox/*
-**/node_modules/
-tools/sim-watcher/dist/
+
+# Legacy / tool-specific
+fastlane/README.md
+fastlane/report.xml
+fastlane/test_output/*
+fastlane/fastlanematch
+AppBinary.xcworkspace/*
+Project.xcodeproj/*
+Watch/Watch.xcodeproj/*
+AppBundle.xcworkspace/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 1a268ae4e1..0000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,196 +0,0 @@
-stages:
- - build
- - deploy
- - verifysanity
- - verify
- - submit
-
-variables:
- LANG: "en_US.UTF-8"
- LC_ALL: "en_US.UTF-8"
- GIT_SUBMODULE_STRATEGY: normal
-
-internal:
- tags:
- - ios_internal
- stage: build
- only:
- - master
- except:
- - tags
- script:
- - 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 --embedWatchApp --watchApiId="$TELEGRAM_WATCHOS_APP_ID" --watchApiHash="$TELEGRAM_WATCHOS_APP_HASH"
- - 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 --embedWatchApp --watchApiId="$TELEGRAM_WATCHOS_APP_ID" --watchApiHash="$TELEGRAM_WATCHOS_APP_HASH"
- - 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
-
-internal_testflight:
- tags:
- - ios_internal
- stage: deploy
- only:
- - master
- except:
- - tags
- script:
- - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64 --embedWatchApp --watchApiId="$TELEGRAM_WATCHOS_APP_ID" --watchApiHash="$TELEGRAM_WATCHOS_APP_HASH"
- - python3 -u build-system/Make/Make.py remote-deploy-testflight --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
- environment:
- name: testflight_llc
- artifacts:
- when: always
- paths:
- - build/artifacts
- expire_in: 1 week
-
-appstore_development:
- tags:
- - ios_internal
- stage: build
- only:
- - appstore-development
- except:
- - tags
- script:
- - 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
- environment:
- name: appstore-development
- artifacts:
- paths:
- - build/artifacts/Telegram.DSYMs.zip
- expire_in: 1 week
-
-experimental_i:
- tags:
- - ios_experimental
- stage: build
- only:
- - experimental-3
- except:
- - tags
- script:
- - 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/Make.py remote-deploy-testflight --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
- environment:
- name: experimental
- artifacts:
- paths:
- - build/artifacts/Telegram.DSYMs.zip
- expire_in: 1 week
-
-experimental:
- tags:
- - ios_internal
- stage: build
- only:
- - experimental-2
- except:
- - tags
- script:
- - 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
- environment:
- name: experimental-2
- artifacts:
- paths:
- - build/artifacts/Telegram.DSYMs.zip
- expire_in: 1 week
-
-beta_testflight:
- tags:
- - ios_beta
- stage: build
- only:
- - beta
- - hotfix
- except:
- - tags
- script:
- - 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/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64 --embedWatchApp --watchApiId="$TELEGRAM_WATCHOS_APP_ID" --watchApiHash="$TELEGRAM_WATCHOS_APP_HASH"
- environment:
- name: testflight_llc
- artifacts:
- paths:
- - build/artifacts
- expire_in: 3 weeks
-
-deploy_beta_testflight:
- tags:
- - ios_beta
- stage: deploy
- only:
- - beta
- - hotfix
- except:
- - tags
- script:
- - python3 -u build-system/Make/Make.py remote-deploy-testflight --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
- environment:
- name: testflight_llc
-
-verifysanity_beta_testflight:
- tags:
- - ios_beta
- stage: verifysanity
- only:
- - beta
- - hotfix
- except:
- - tags
- script:
- - rm -rf build/verify-input && mkdir -p build/verify-input && mv build/artifacts/Telegram.ipa build/verify-input/TelegramVerifySource.ipa
- - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64
- - python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa"
- - if [ $? -ne 0 ]; then echo "Verification failed"; mkdir -p build/verifysanity_artifacts; cp build/artifacts/Telegram.ipa build/verifysanity_artifacts/; exit 1; fi
- environment:
- name: testflight_llc
- artifacts:
- when: on_failure
- paths:
- - build/artifacts
- expire_in: 1 week
-
-verify_beta_testflight:
- tags:
- - ios_beta
- stage: verify
- only:
- - beta
- - hotfix
- except:
- - tags
- script:
- - rm -rf build/verify-input && mkdir -p build/verify-input && mv build/artifacts/Telegram.ipa build/verify-input/TelegramVerifySource.ipa
- - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64
- - python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa"
- - if [ $? -ne 0 ]; then echo "Verification failed"; mkdir -p build/verify_artifacts; cp build/artifacts/Telegram.ipa build/verify_artifacts/; exit 1; fi
- environment:
- name: testflight_llc
- artifacts:
- when: on_failure
- paths:
- - build/artifacts
- expire_in: 1 week
-
-submit_appstore:
- tags:
- - deploy
- only:
- - beta
- - hotfix
- stage: submit
- needs: []
- when: manual
- script:
- - sh "$TELEGRAM_SUBMIT_BUILD"
- environment:
- name: testflight_llc
diff --git a/.gitmodules b/.gitmodules
index 54ccfe3516..a8e9af28d0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,7 +1,7 @@
[submodule "submodules/rlottie/rlottie"]
path = submodules/rlottie/rlottie
- url=../rlottie.git
+ url = https://github.com/TelegramMessenger/rlottie.git
[submodule "build-system/bazel-rules/rules_apple"]
path = build-system/bazel-rules/rules_apple
url=https://github.com/ali-fareed/rules_apple.git
@@ -13,7 +13,7 @@ url=https://github.com/bazelbuild/rules_swift.git
url = https://github.com/bazelbuild/apple_support.git
[submodule "submodules/TgVoipWebrtc/tgcalls"]
path = submodules/TgVoipWebrtc/tgcalls
-url=../tgcalls.git
+ url = https://github.com/TelegramMessenger/tgcalls.git
[submodule "third-party/libvpx/libvpx"]
path = third-party/libvpx/libvpx
url = https://github.com/webmproject/libvpx.git
diff --git a/CLAUDE.md b/CLAUDE.md
index adfca9c415..e6749f70e6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,7 +6,9 @@ This file provides guidance to AI assistants when working with code in this repo
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:**
+**Convenience wrapper:** `scripts/build-wintergram.sh` wraps the Make.py invocations for the WinterGram dev config and emits WinterGram-named IPAs in `build/`: `sim` (simulator), `sideload` / `livecontainer` (device), `all`, plus `--install`/`--run` (simulator-only: build + install into the booted Simulator), `--clean`, `--open-build-dir`. It sources `~/.zshrc` itself. App icons are regenerated from `branding/wnt-app-icon-*.png` by `scripts/generate-app-icons.sh`.
+
+**Raw command (canonical):**
```sh
python3 build-system/Make/Make.py --overrideXcodeVersion \
diff --git a/README.md b/README.md
index 2ba6771007..7c6f498e34 100644
--- a/README.md
+++ b/README.md
@@ -1,122 +1,102 @@
-
WinterGram
-
-WinterGram (Wnt) is a feature-rich, privacy-focused Telegram client for iPhone — a native iOS port of the AyuGram experience, built on top of Telegram-iOS.
+# WinterGram
-
-
-
-
-
-
+
----
+
+ Privacy-focused Telegram client for iOS
+
-**WinterGram** brings the most-loved AyuGram features to iOS with a clean, Material-inspired interface, a configurable **Liquid Glass** appearance, and a single dedicated settings tab where everything lives. It speaks both the standard `tg://` deep links and its own `wnt://` scheme.
+
+
+
+
+
+
+
+
+WinterGram (Wnt) is an independent iOS client for Telegram. It keeps the familiar Telegram experience and adds a dedicated **WinterGram** settings tab where privacy tools, history recovery, appearance controls, and other enhancements live in one place.
+
+The app speaks both `tg://` deep links and its own `wnt://` scheme.
---
-## ✨ Features
+## Download
-### 👻 Privacy & Ghost Mode
-- **Ghost Mode**: don't send read receipts, typing/upload status, or online presence — toggle it all at once.
-- **Send without sound**: never / only in Ghost Mode / always.
-- **Story ghost**: view stories without marking them seen, with an optional confirmation prompt.
-- **Mark read after action**, **go offline after going online**, and per-toggle locks.
+Prebuilt unsigned IPAs are published on the [Releases](../../releases) page. Install with [AltStore](https://altstore.io), [SideStore](https://sidestore.io), or another sideloading tool.
-### 🗂 History & Recovery
-- **Save deleted messages**: keep messages locally even after the other side deletes them.
-- **Edit history**: store every revision of a message and browse it.
-- **Semi-transparent deleted markers** and a customizable deleted / edited mark.
-
-### 🧊 Hidden Archive ("AАrchive")
-- Stash chats into a **separate, settings-only archive** — no notifications, no badge.
-- Optional **auto-mark-read** for everything sent to the stash.
-
-### 🛡️ Anti-Features
-- **Disable ads** (sponsored messages).
-- **Local Telegram Premium** — unlock Premium-gated UI locally.
-- **Shadow ban** — silently hide a user's messages from your view.
-- **Hide / disable stories**, **hide Premium statuses**, **disable open-link warning**.
-
-### 💬 Chat Conveniences
-- **Sticker / GIF / voice send confirmations**.
-- **Message seconds** in timestamps and **peer ID** display (Telegram or Bot API form).
-- **Message translation** with a selectable provider (Telegram / Google / Yandex / system).
-- **WebView platform spoofing** (auto / iOS / Android / macOS / desktop) and taller WebViews.
-
-### 🎨 Appearance & Customization
-- **Liquid Glass** — frosted, translucent surfaces across the chat list, navigation, and tab bar, with **on/off toggle**, adjustable **transparency**, **blur radius**, **tint**, and per-surface application.
-- **Material Design** switches and controls.
-- **Avatar corner radius** (round → squircle → square) and **message bubble radius**, with optional single-corner mode.
-- **Custom fonts** (UI + monospace).
-- **App icons**, including the bundled WinterGram dark icon, plus **AyuGram / exteraGram icon-pack compatibility**.
-- **Custom emoji** support, with an option to show only your added emoji & stickers.
+WinterGram is not distributed on the App Store. Free Apple IDs must re-sign the app every 7 days; AltStore and SideStore can automate this.
---
-## 🔗 Deep Links
+## Features
-WinterGram registers and resolves two URL schemes — anything that works with `tg://` works with `wnt://`:
+WinterGram adds Ghost Mode, saved deleted messages, edit history, a hidden archive, local Premium UI, ad removal, Liquid Glass appearance, spoofing, chat conveniences, and more.
+
+A complete, structured feature list is in [`docs/FEATURES.md`](docs/FEATURES.md).
+
+Developer implementation notes: [`docs/wintergram-features.md`](docs/wintergram-features.md).
+
+---
+
+## Quick Start (build from source)
+
+**Requirements:** macOS, Xcode, Python 3, ~60 GB free disk space.
+
+```sh
+git clone --recursive https://github.com/reekeer/WinterGram.git
+cd WinterGram
+cp build-system/wintergram-development-configuration.example.json \
+ build-system/wintergram-development-configuration.json
+# Edit the JSON: api_id, api_hash, bundle_id, team_id
+./scripts/build-wintergram.sh sim
+```
+
+The simulator IPA lands in `build/WinterGram-Simulator.ipa`. Build straight into a running Simulator with `./scripts/build-wintergram.sh --install` (add `--run` to launch it). Full instructions (device builds, signing, AltStore): [`docs/wintergram-setup.md`](docs/wintergram-setup.md).
+
+---
+
+## Configuration
+
+All WinterGram options are stored in `WinterGramSettings` and exposed through the WinterGram tab in Settings. English UI strings ship in `en.lproj`; Russian translations are seeded in `submodules/TelegramPresentationData/Sources/WinterGramStrings.swift`.
+
+---
+
+## Project Layout
+
+```
+WinterGram/
+├── Telegram/ App entry, extensions, icons, xcconfig
+├── submodules/ Feature libraries (Swift / Obj-C)
+├── branding/ Source art: app-icon PNGs (wnt-app-icon-*.png) + badge/snowflake shapes
+├── docs/ Setup guide, feature list, architecture notes
+├── build-system/ Bazel wrapper (Make.py) and configs
+└── scripts/ Build + tooling
+ ├── build-wintergram.sh Convenience build script (sim / sideload / livecontainer)
+ └── generate-app-icons.sh Regenerate every app icon from branding/wnt-app-icon-*.png
+```
+
+---
+
+## Deep Links
+
+Anything that works with `tg://` also works with `wnt://`:
```
tg://resolve?domain=durov
wnt://resolve?domain=durov
+wnt://wintergram/ghost
```
-`wnt://` links are normalized to the standard resolver at the entry point, so they route through exactly the same handling as native Telegram links.
+`wnt://` URLs are normalized to `tg://` at the app entry point.
---
-## 🚀 Build
+## Contributing
-WinterGram builds with the standard Telegram-iOS toolchain (Bazel via the `Make.py` wrapper) on **macOS with Xcode**.
-
-```sh
-python3 build-system/Make/Make.py --overrideXcodeVersion \
- --cacheDir ~/telegram-bazel-cache \
- build \
- --configurationPath build-system/appstore-configuration.json \
- --gitCodesigningRepository \
- --gitCodesigningType development --gitCodesigningUseCurrent \
- --buildNumber=1 --configuration=debug_sim_arm64
-```
-
-See [`docs/wintergram-features.md`](docs/wintergram-features.md) for the feature → implementation map and the project's architecture notes.
+Maintainers: [**IMDelewer**](https://github.com/IMDelewer), [**salenyo**](https://github.com/salenyo) under the [reekeer](https://github.com/reekeer) organization. See [`MAINTAINERS.md`](MAINTAINERS.md).
---
-## ⚙️ Configuration
-
-All WinterGram options live in a single settings store (`WinterGramSettings`), persisted with the app's shared-data system and exposed through reactive signals. There is one dedicated **WinterGram** tab in Settings — no scattered toggles.
-
----
-
-## 🗂 Structure
-
-```
-WinterGram/
-├── Telegram/ ← App entry points and extensions
-├── submodules/ ← Feature libraries (Swift / Obj-C)
-│ └── TelegramUIPreferences/
-│ └── Sources/WinterGramSettings.swift ← all WinterGram options
-├── branding/ ← WinterGram icons and brand assets
-├── docs/ ← Architecture and feature documentation
-├── build-system/ ← Bazel build wrapper (Make.py)
-└── README.md
-```
-
----
-
-## 🤝 Contributing
-
-Contributions are welcome. WinterGram is maintained by [**IMDelewer**](https://github.com/IMDelewer) and [**salenyo**](https://github.com/salenyo) under the [reekeer](https://github.com/reekeer) organization. See [`MAINTAINERS.md`](MAINTAINERS.md).
-
----
-
-
- Built on Telegram-iOS · inspired by AyuGram
-
-
-GPLv2 © reekeer
+GPLv2 © reekeer
diff --git a/Random.txt b/Random.txt
deleted file mode 100644
index 1860059bf9..0000000000
--- a/Random.txt
+++ /dev/null
@@ -1 +0,0 @@
-c27f02cf6e413fdc
diff --git a/Telegram/BUILD b/Telegram/BUILD
index ef5fa5dbdf..4231b24d96 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -317,21 +317,25 @@ filegroup(
)
alternate_icon_folders = [
- "BlackIcon",
"BlackClassicIcon",
"BlackFilledIcon",
- "BlueIcon",
+ "BlackIcon",
"BlueClassicIcon",
"BlueFilledIcon",
- "WhiteFilledIcon",
+ "BlueIcon",
"New1",
"New2",
"Premium",
"PremiumBlack",
"PremiumTurbo",
+ "WinterGramDark",
+ "WinterGramDeveloper",
+ "WinterGramHouseDark",
+ "WinterGramHouseLight",
+ "WinterGramLight",
]
-composer_icon_folders = ["Telegram"]
+composer_icon_folders = []
[
filegroup(
@@ -434,6 +438,16 @@ plist_fragment(
tonsite
+
+ CFBundleTypeRole
+ Viewer
+ CFBundleURLName
+ {telegram_bundle_id}.wnt
+ CFBundleURLSchemes
+
+ wnt
+
+
""".format(
telegram_bundle_id = telegram_bundle_id,
@@ -525,6 +539,26 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id
"""
+background_modes_fragment = """
+ UIBackgroundModes
+
+ audio
+ fetch
+ location
+ remote-notification
+ voip
+ processing
+
+""" if telegram_aps_environment != "" else """
+ UIBackgroundModes
+
+ audio
+ fetch
+ location
+ processing
+
+"""
+
siri_fragment = "" if not telegram_enable_siri else """
com.apple.developer.siri
@@ -629,7 +663,7 @@ plist_fragment(
template =
"""
CFBundleDisplayName
- Telegram
+ WinterGram
"""
)
@@ -955,14 +989,14 @@ ios_framework(
plist_fragment(
name = "ShareInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.Share
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1047,14 +1081,14 @@ ios_extension(
plist_fragment(
name = "NotificationContentInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.NotificationContent
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1154,14 +1188,14 @@ ios_extension(
plist_fragment(
name = "WidgetInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.Widget
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1267,14 +1301,14 @@ ios_extension(
plist_fragment(
name = "IntentsInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.SiriIntents
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1398,14 +1432,14 @@ ios_extension(
plist_fragment(
name = "BroadcastUploadInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.BroadcastUpload
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1492,14 +1526,14 @@ ios_extension(
plist_fragment(
name = "NotificationServiceInfoPlist",
extension = "plist",
- template =
+ template =
"""
CFBundleDevelopmentRegion
en
CFBundleIdentifier
{telegram_bundle_id}.NotificationService
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
XPC!
NSExtension
@@ -1555,7 +1589,7 @@ ios_extension(
plist_fragment(
name = "TelegramInfoPlist",
extension = "plist",
- template =
+ template =
"""
BGTaskSchedulerPermittedIdentifiers
@@ -1568,11 +1602,11 @@ plist_fragment(
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- Telegram
+ WinterGram
CFBundleIdentifier
{telegram_bundle_id}
CFBundleName
- Telegram
+ WinterGram
CFBundlePackageType
APPL
CFBundleSignature
@@ -1629,17 +1663,17 @@ plist_fragment(
NSCameraUsageDescription
We need this so that you can take and share photos and videos.
NSContactsUsageDescription
- Telegram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
+ WinterGram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
NSFaceIDUsageDescription
You can use Face ID to unlock the app.
NSLocationAlwaysUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map. You also need this to send locations from an Apple Watch.
+ When you send your location to your friends, WinterGram needs access to show them a map. You also need this to send locations from an Apple Watch.
NSLocationWhenInUseUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSMicrophoneUsageDescription
We need this so that you can record and share voice messages and videos with sound.
NSMotionUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSPhotoLibraryAddUsageDescription
We need this so that you can share photos and videos from your photo library.
NSPhotoLibraryUsageDescription
@@ -1658,15 +1692,7 @@ plist_fragment(
AremacFS-Regular.otf
AremacFS-Semibold.otf
- UIBackgroundModes
-
- audio
- fetch
- location
- remote-notification
- voip
- processing
-
+{background_modes_fragment}
UIDeviceFamily
1
@@ -1707,13 +1733,13 @@ plist_fragment(
public.data
UTTypeDescription
- Telegram iOS Color Theme File
+ WinterGram iOS Color Theme File
UTTypeIconFiles
BlueIcon@3x.png
UTTypeIdentifier
- org.telegram.Telegram-iOS.theme
+ dev.reekeer.wintergram.theme
UTTypeTagSpecification
public.filename-extension
@@ -1729,6 +1755,7 @@ plist_fragment(
""".format(
telegram_bundle_id = telegram_bundle_id,
+ background_modes_fragment = background_modes_fragment,
)
)
@@ -1777,12 +1804,13 @@ ios_application(
":UrlTypesInfoPlist",
],
app_icons = [ ":{}_icon".format(name) for name in composer_icon_folders ],
+ primary_app_icon = "WinterGramDarkIcon",
alternate_icons = [
":{}".format(name) for name in alternate_icon_folders
],
resources = [
":LaunchScreen",
- #":DefaultAppIcon",
+ ":DefaultAppIcon",
],
frameworks = [
":MtProtoKitFramework",
@@ -1845,9 +1873,9 @@ filegroup(
)
ios_test_runner(
- name = "iPhone-17__26.2",
+ name = "iPhone-17__26.5",
device_type = "iPhone 17",
- os_version = "26.2",
+ os_version = "26.5",
)
@@ -1864,7 +1892,7 @@ ios_ui_test_suite(
bundle_id = "org.telegram.Telegram-iOS-uitests",
minimum_os_version = minimum_os_version,
runners = [
- ":iPhone-17__26.2",
+ ":iPhone-17__26.5",
],
tags = ["manual"],
test_host = "//Telegram",
diff --git a/Telegram/Telegram-iOS/AlternateIcons-iPad.plist b/Telegram/Telegram-iOS/AlternateIcons-iPad.plist
index a9ebd9173d..b62e1e2306 100644
--- a/Telegram/Telegram-iOS/AlternateIcons-iPad.plist
+++ b/Telegram/Telegram-iOS/AlternateIcons-iPad.plist
@@ -72,7 +72,29 @@
UIPrerenderedIcon
- New1
+ WinterGramDark
+
+ CFBundleIconFiles
+
+ WinterGramDarkIpad
+ WinterGramDarkLargeIpad
+ WinterGramDarkNotificationIcon
+
+ UIPrerenderedIcon
+
+
+ WinterGramLight
+
+ CFBundleIconFiles
+
+ WinterGramLightIpad
+ WinterGramLightLargeIpad
+ WinterGramLightNotificationIcon
+
+ UIPrerenderedIcon
+
+
+ New1
CFBundleIconFiles
diff --git a/Telegram/Telegram-iOS/AlternateIcons.plist b/Telegram/Telegram-iOS/AlternateIcons.plist
index d97e8c714a..ec44e1711d 100644
--- a/Telegram/Telegram-iOS/AlternateIcons.plist
+++ b/Telegram/Telegram-iOS/AlternateIcons.plist
@@ -66,6 +66,26 @@
UIPrerenderedIcon
+ WinterGramDark
+
+ CFBundleIconFiles
+
+ WinterGramDark
+ WinterGramDarkNotificationIcon
+
+ UIPrerenderedIcon
+
+
+ WinterGramLight
+
+ CFBundleIconFiles
+
+ WinterGramLight
+ WinterGramLightNotificationIcon
+
+ UIPrerenderedIcon
+
+
New1
CFBundleIconFiles
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/Contents.json
new file mode 100644
index 0000000000..8a71db8fa4
--- /dev/null
+++ b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramDark@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramDark@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@2x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@2x.png
new file mode 100644
index 0000000000..f6328bfc03
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@2x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@3x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@3x.png
new file mode 100644
index 0000000000..8c495fab69
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDark.imageset/WinterGramDark@3x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/Contents.json
new file mode 100644
index 0000000000..02debbbd84
--- /dev/null
+++ b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramDeveloper@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramDeveloper@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@2x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@2x.png
new file mode 100644
index 0000000000..4b3174ab35
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@2x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@3x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@3x.png
new file mode 100644
index 0000000000..d14a6f7dd4
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramDeveloper.imageset/WinterGramDeveloper@3x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/Contents.json
new file mode 100644
index 0000000000..53b8c63624
--- /dev/null
+++ b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramHouseDark@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramHouseDark@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@2x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@2x.png
new file mode 100644
index 0000000000..cfcad99d96
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@2x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@3x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@3x.png
new file mode 100644
index 0000000000..31902ab7dc
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseDark.imageset/WinterGramHouseDark@3x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/Contents.json
new file mode 100644
index 0000000000..132c05d07d
--- /dev/null
+++ b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramHouseLight@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramHouseLight@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@2x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@2x.png
new file mode 100644
index 0000000000..b6d9c255ad
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@2x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@3x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@3x.png
new file mode 100644
index 0000000000..3719819bc7
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramHouseLight.imageset/WinterGramHouseLight@3x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/Contents.json
new file mode 100644
index 0000000000..4be036864f
--- /dev/null
+++ b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramLight@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramLight@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@2x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@2x.png
new file mode 100644
index 0000000000..24c435a6dc
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@2x.png differ
diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@3x.png b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@3x.png
new file mode 100644
index 0000000000..4d6aceafdd
Binary files /dev/null and b/Telegram/Telegram-iOS/AppIcons.xcassets/WinterGramLight.imageset/WinterGramLight@3x.png differ
diff --git a/Telegram/Telegram-iOS/Config-AppStoreLLC.xcconfig b/Telegram/Telegram-iOS/Config-AppStoreLLC.xcconfig
index 76c1f1e594..d91a311569 100644
--- a/Telegram/Telegram-iOS/Config-AppStoreLLC.xcconfig
+++ b/Telegram/Telegram-iOS/Config-AppStoreLLC.xcconfig
@@ -1,4 +1,4 @@
-APP_NAME=Telegram
+APP_NAME=WinterGram
APP_BUNDLE_ID=ph.telegra.Telegraph
APP_SPECIFIC_URL_SCHEME=tgapp
diff --git a/Telegram/Telegram-iOS/Config-Fork.xcconfig b/Telegram/Telegram-iOS/Config-Fork.xcconfig
index c3192251c1..8a190ee5b8 100644
--- a/Telegram/Telegram-iOS/Config-Fork.xcconfig
+++ b/Telegram/Telegram-iOS/Config-Fork.xcconfig
@@ -1,4 +1,4 @@
-APP_NAME=Telegram Fork
+APP_NAME=WinterGram
APP_BUNDLE_ID=fork.telegram.Fork
APP_SPECIFIC_URL_SCHEME=tgfork
diff --git a/Telegram/Telegram-iOS/Config-WinterGram.xcconfig b/Telegram/Telegram-iOS/Config-WinterGram.xcconfig
new file mode 100644
index 0000000000..aca6ac860a
--- /dev/null
+++ b/Telegram/Telegram-iOS/Config-WinterGram.xcconfig
@@ -0,0 +1,8 @@
+APP_NAME=WinterGram
+APP_BUNDLE_ID=dev.reekeer.wintergram
+APP_SPECIFIC_URL_SCHEME=wnt
+
+GLOBAL_CONSTANTS = APP_CONFIG_IS_INTERNAL_BUILD=true APP_CONFIG_IS_APPSTORE_BUILD=false APP_CONFIG_APPSTORE_ID=0 APP_SPECIFIC_URL_SCHEME="\"$(APP_SPECIFIC_URL_SCHEME)\""
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) $(GLOBAL_CONSTANTS)
+
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) APP_CONFIG_API_ID=2040 APP_CONFIG_API_HASH="\"b18441a1ff607e10a989891a5462e627\"" APP_CONFIG_HOCKEYAPP_ID=""
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json
index 4d65457087..72a8d5035b 100644
--- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json
@@ -1,115 +1,110 @@
{
- "images" : [
+ "images": [
{
- "filename" : "BlueNotificationIcon@2x.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "20x20"
+ "filename": "BlueNotificationIcon@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
},
{
- "filename" : "BlueNotificationIcon@3x.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "20x20"
+ "filename": "BlueNotificationIcon@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
},
{
- "filename" : "Simple@58x58.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "29x29"
+ "filename": "Simple@58x58.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
},
{
- "filename" : "Simple@87x87.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "29x29"
+ "filename": "Simple@87x87.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
},
{
- "filename" : "Simple@80x80.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "40x40"
+ "filename": "Simple@80x80.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
},
{
- "filename" : "BlueIcon@2x-1.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "40x40"
+ "filename": "BlueIcon@2x-1.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
},
{
- "filename" : "BlueIcon@2x.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "60x60"
+ "filename": "BlueIcon@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
},
{
- "filename" : "BlueIcon@3x.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "60x60"
+ "filename": "BlueIcon@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
},
{
- "filename" : "BlueNotificationIcon.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "20x20"
+ "filename": "BlueNotificationIcon.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "20x20"
},
{
- "filename" : "BlueNotificationIcon@2x-1.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "20x20"
+ "filename": "BlueNotificationIcon@2x-1.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "20x20"
},
{
- "filename" : "Simple@29x29.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "29x29"
+ "filename": "Simple@29x29.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "29x29"
},
{
- "filename" : "Simple@58x58-1.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "29x29"
+ "filename": "Simple@58x58-1.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "29x29"
},
{
- "filename" : "Simple@40x40-1.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "40x40"
+ "filename": "Simple@40x40-1.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "40x40"
},
{
- "filename" : "Simple@80x80-1.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "40x40"
+ "filename": "Simple@80x80-1.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "40x40"
},
{
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "76x76"
+ "filename": "BlueIconIpad@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "76x76"
},
{
- "filename" : "BlueIconIpad@2x.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "76x76"
+ "filename": "BlueIconLargeIpad@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "83.5x83.5"
},
{
- "filename" : "BlueIconLargeIpad@2x.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "83.5x83.5"
- },
- {
- "filename" : "Simple-iTunesArtwork.png",
- "idiom" : "ios-marketing",
- "scale" : "1x",
- "size" : "1024x1024"
+ "filename": "Simple-iTunesArtwork.png",
+ "idiom": "ios-marketing",
+ "scale": "1x",
+ "size": "1024x1024"
}
],
- "info" : {
- "author" : "xcode",
- "version" : 1
+ "info": {
+ "author": "xcode",
+ "version": 1
}
}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/Contents.json
new file mode 100644
index 0000000000..09e3c1d8b1
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramBadge@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramBadge@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@2x.png
new file mode 100644
index 0000000000..cf12532c88
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@2x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@3x.png
new file mode 100644
index 0000000000..47081de31d
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramBadge.imageset/WinterGramBadge@3x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Contents.json
index 172ed83a1f..e5f794209c 100644
--- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Contents.json
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Contents.json
@@ -84,12 +84,6 @@
"scale" : "2x",
"size" : "40x40"
},
- {
- "filename" : "Icon-76.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "76x76"
- },
{
"filename" : "Icon-152.png",
"idiom" : "ipad",
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-1024.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-1024.png
index 7a301e6332..4b5ccee658 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-1024.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-1024.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-120.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-120.png
index a65b5897a8..f6328bfc03 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-120.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-120.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-152.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-152.png
index dd7a6f1dd6..4af76d3dd5 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-152.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-152.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-167.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-167.png
index 02d5cf0e9d..56c37d1f8e 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-167.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-167.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-180.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-180.png
index 926d014f30..8c495fab69 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-180.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-180.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-20.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-20.png
index 6aee2e35c4..8ef699a0b3 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-20.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-20.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-29.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-29.png
index 1fea6d62a4..41f8e796af 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-29.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-29.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-40.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-40.png
index dcd3c0784f..b25cd5f428 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-40.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-40.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-58.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-58.png
index cd11b73ca2..f7adbb3ada 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-58.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-58.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-60.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-60.png
index 294da6cf0a..edda6a53d0 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-60.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-60.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-76.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-76.png
deleted file mode 100644
index 200a039341..0000000000
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-76.png and /dev/null differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-80.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-80.png
index d1c6ea5932..db01075308 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-80.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-80.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-87.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-87.png
index ede13b9a80..8d53b9279d 100644
Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-87.png and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset/Icon-87.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/Contents.json
new file mode 100644
index 0000000000..275b802d69
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WinterGramSnowflake@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WinterGramSnowflake@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@2x.png
new file mode 100644
index 0000000000..1b266a029c
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@2x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@3x.png
new file mode 100644
index 0000000000..3a51f589ef
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramSnowflake.imageset/WinterGramSnowflake@3x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/Contents.json
new file mode 100644
index 0000000000..d558a93bad
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/Contents.json
@@ -0,0 +1,6 @@
+{
+ "images" : [
+ { "filename" : "WntGramBackplateShape.png", "idiom" : "universal", "scale" : "1x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/WntGramBackplateShape.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/WntGramBackplateShape.png
new file mode 100644
index 0000000000..0820196879
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBackplateShape.imageset/WntGramBackplateShape.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/Contents.json
new file mode 100644
index 0000000000..a96dbf15f6
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WntGramBanner@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WntGramBanner@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@2x.png
new file mode 100644
index 0000000000..5dae6791c9
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@2x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@3x.png
new file mode 100644
index 0000000000..3c7b3420cc
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBanner.imageset/WntGramBanner@3x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/Contents.json
new file mode 100644
index 0000000000..742d28e5ad
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WntGramBannerDefault@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WntGramBannerDefault@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@2x.png
new file mode 100644
index 0000000000..ef9e989a34
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@2x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@3x.png
new file mode 100644
index 0000000000..80b5b50679
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramBannerDefault.imageset/WntGramBannerDefault@3x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/Contents.json
new file mode 100644
index 0000000000..d4ac40ec1d
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "WntGramDeveloperBadge@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "WntGramDeveloperBadge@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@2x.png
new file mode 100644
index 0000000000..e62ae8e755
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@2x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@3x.png
new file mode 100644
index 0000000000..971cce5e4f
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramDeveloperBadge.imageset/WntGramDeveloperBadge@3x.png differ
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/Contents.json
new file mode 100644
index 0000000000..77016ad4ed
--- /dev/null
+++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/Contents.json
@@ -0,0 +1,6 @@
+{
+ "images" : [
+ { "filename" : "WntGramSnowflakeShape.png", "idiom" : "universal", "scale" : "1x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/WntGramSnowflakeShape.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/WntGramSnowflakeShape.png
new file mode 100644
index 0000000000..bf2b21321b
Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WntGramSnowflakeShape.imageset/WntGramSnowflakeShape.png differ
diff --git a/Telegram/Telegram-iOS/Info.plist b/Telegram/Telegram-iOS/Info.plist
index 389f866c2b..e603c8d817 100644
--- a/Telegram/Telegram-iOS/Info.plist
+++ b/Telegram/Telegram-iOS/Info.plist
@@ -85,11 +85,31 @@
UIPrerenderedIcon
+ WinterGramDark
+
+ CFBundleIconFiles
+
+ WinterGramDark
+ WinterGramDarkNotificationIcon
+
+ UIPrerenderedIcon
+
+
+ WinterGramLight
+
+ CFBundleIconFiles
+
+ WinterGramLight
+ WinterGramLightNotificationIcon
+
+ UIPrerenderedIcon
+
+
CFBundlePrimaryIcon
CFBundleIconName
- AppIconLLC
+ WinterGramDarkIcon
UIPrerenderedIcon
@@ -171,11 +191,33 @@
UIPrerenderedIcon
+ WinterGramDark
+
+ CFBundleIconFiles
+
+ WinterGramDarkIpad
+ WinterGramDarkLargeIpad
+ WinterGramDarkNotificationIcon
+
+ UIPrerenderedIcon
+
+
+ WinterGramLight
+
+ CFBundleIconFiles
+
+ WinterGramLightIpad
+ WinterGramLightLargeIpad
+ WinterGramLightNotificationIcon
+
+ UIPrerenderedIcon
+
+
CFBundlePrimaryIcon
CFBundleIconName
- AppIconLLC
+ WinterGramDarkIcon
UIPrerenderedIcon
@@ -277,17 +319,17 @@
NSCameraUsageDescription
We need this so that you can take and share photos and videos.
NSContactsUsageDescription
- Telegram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
+ WinterGram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
NSFaceIDUsageDescription
You can use Face ID to unlock the app.
NSLocationAlwaysUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map. You also need this to send locations from an Apple Watch.
+ When you send your location to your friends, WinterGram needs access to show them a map. You also need this to send locations from an Apple Watch.
NSLocationWhenInUseUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSMicrophoneUsageDescription
We need this so that you can record and share voice messages and videos with sound.
NSMotionUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSPhotoLibraryAddUsageDescription
We need this so that you can share photos and videos from your photo library.
NSPhotoLibraryUsageDescription
@@ -355,13 +397,13 @@
public.data
UTTypeDescription
- Telegram iOS Color Theme File
+ WinterGram iOS Color Theme File
UTTypeIconFiles
BlueIcon@3x.png
UTTypeIdentifier
- org.telegram.Telegram-iOS.theme
+ dev.reekeer.wintergram.theme
UTTypeTagSpecification
public.filename-extension
diff --git a/Telegram/Telegram-iOS/InfoBazel.plist b/Telegram/Telegram-iOS/InfoBazel.plist
index e9b91c4c41..59c7067a11 100644
--- a/Telegram/Telegram-iOS/InfoBazel.plist
+++ b/Telegram/Telegram-iOS/InfoBazel.plist
@@ -111,17 +111,17 @@
NSCameraUsageDescription
We need this so that you can take and share photos and videos.
NSContactsUsageDescription
- Telegram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
+ WinterGram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices.
NSFaceIDUsageDescription
You can use Face ID to unlock the app.
NSLocationAlwaysUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map. You also need this to send locations from an Apple Watch.
+ When you send your location to your friends, WinterGram needs access to show them a map. You also need this to send locations from an Apple Watch.
NSLocationWhenInUseUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSMicrophoneUsageDescription
We need this so that you can record and share voice messages and videos with sound.
NSMotionUsageDescription
- When you send your location to your friends, Telegram needs access to show them a map.
+ When you send your location to your friends, WinterGram needs access to show them a map.
NSPhotoLibraryAddUsageDescription
We need this so that you can share photos and videos from your photo library.
NSPhotoLibraryUsageDescription
@@ -189,13 +189,13 @@
public.data
UTTypeDescription
- Telegram iOS Color Theme File
+ WinterGram iOS Color Theme File
UTTypeIconFiles
BlueIcon@3x.png
UTTypeIdentifier
- org.telegram.Telegram-iOS.theme
+ dev.reekeer.wintergram.theme
UTTypeTagSpecification
public.filename-extension
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@2x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@2x.png
new file mode 100644
index 0000000000..f6328bfc03
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@3x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@3x.png
new file mode 100644
index 0000000000..8c495fab69
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDark@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad.png
new file mode 100644
index 0000000000..305dd817aa
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad@2x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad@2x.png
new file mode 100644
index 0000000000..4af76d3dd5
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkLargeIpad@2x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkLargeIpad@2x.png
new file mode 100644
index 0000000000..56c37d1f8e
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkLargeIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon.png
new file mode 100644
index 0000000000..8ef699a0b3
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@2x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@2x.png
new file mode 100644
index 0000000000..b25cd5f428
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@3x.png b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@3x.png
new file mode 100644
index 0000000000..edda6a53d0
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDark.alticon/WinterGramDarkNotificationIcon@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@2x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@2x.png
new file mode 100644
index 0000000000..4b3174ab35
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@3x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@3x.png
new file mode 100644
index 0000000000..d14a6f7dd4
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloper@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad.png
new file mode 100644
index 0000000000..936b425472
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad@2x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad@2x.png
new file mode 100644
index 0000000000..ccf576b4eb
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperLargeIpad@2x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperLargeIpad@2x.png
new file mode 100644
index 0000000000..ace48b8d33
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperLargeIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon.png
new file mode 100644
index 0000000000..bd50dc216a
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@2x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@2x.png
new file mode 100644
index 0000000000..f8493a29b0
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@3x.png b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@3x.png
new file mode 100644
index 0000000000..b0b62189c0
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramDeveloper.alticon/WinterGramDeveloperNotificationIcon@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@2x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@2x.png
new file mode 100644
index 0000000000..cfcad99d96
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@3x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@3x.png
new file mode 100644
index 0000000000..31902ab7dc
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDark@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad.png
new file mode 100644
index 0000000000..0b67ec3c53
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad@2x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad@2x.png
new file mode 100644
index 0000000000..ff20c97c8d
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkLargeIpad@2x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkLargeIpad@2x.png
new file mode 100644
index 0000000000..49f23b8af0
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkLargeIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon.png
new file mode 100644
index 0000000000..8eba748c25
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@2x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@2x.png
new file mode 100644
index 0000000000..3c60717624
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@3x.png b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@3x.png
new file mode 100644
index 0000000000..3adabbc319
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseDark.alticon/WinterGramHouseDarkNotificationIcon@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@2x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@2x.png
new file mode 100644
index 0000000000..b6d9c255ad
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@3x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@3x.png
new file mode 100644
index 0000000000..3719819bc7
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLight@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad.png
new file mode 100644
index 0000000000..b6230a3a9a
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad@2x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad@2x.png
new file mode 100644
index 0000000000..c05e486ca2
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightLargeIpad@2x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightLargeIpad@2x.png
new file mode 100644
index 0000000000..5846d8bb08
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightLargeIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon.png
new file mode 100644
index 0000000000..056decb375
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@2x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@2x.png
new file mode 100644
index 0000000000..a293a3291e
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@3x.png b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@3x.png
new file mode 100644
index 0000000000..2f30936cc3
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramHouseLight.alticon/WinterGramHouseLightNotificationIcon@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@2x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@2x.png
new file mode 100644
index 0000000000..24c435a6dc
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@3x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@3x.png
new file mode 100644
index 0000000000..4d6aceafdd
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLight@3x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad.png
new file mode 100644
index 0000000000..0294b8550e
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad@2x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad@2x.png
new file mode 100644
index 0000000000..4fa008ce3a
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightLargeIpad@2x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightLargeIpad@2x.png
new file mode 100644
index 0000000000..3ff6768694
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightLargeIpad@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon.png
new file mode 100644
index 0000000000..784fde47ea
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@2x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@2x.png
new file mode 100644
index 0000000000..cade06a3ca
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@3x.png b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@3x.png
new file mode 100644
index 0000000000..ee896c516f
Binary files /dev/null and b/Telegram/Telegram-iOS/WinterGramLight.alticon/WinterGramLightNotificationIcon@3x.png differ
diff --git a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings
index 174276a904..2212661071 100644
--- a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"CFBundleDisplayName" = "تيليجرام";
-"NSContactsUsageDescription" = "سيقوم تيليجرام برفع جهات الاتصال الخاصة بك باستمرار إلى خوادم التخزين السحابية ذات التشفير العالي لتتمكن من التواصل مع أصدقائك من خلال جميع أجهزتك.";
-"NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، تيليجرام يحتاج لصلاحيات لعرض الخريطة لهم.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "عندما تختار أن تشارك مكانك بشكل حي مع أصدقائك في المحادثة، يحتاج تيليجرام إلى الوصول لموقعك في الخلفية حتى بعد إغلاق تيليجرام خلال فترة المشاركة.";
-"NSLocationAlwaysUsageDescription" = "عندما تقوم بمشاركة موقعك مع أصدقائك، تيليجرام يحتاج إلى الصلاحية ليعرض لهم الخريطة. كما تحتاج لإعطاء تيليجرام الصلاحية لتتمكن من إرسال موقعك من ساعة آبل.";
+"CFBundleDisplayName" = "WinterGram";
+"NSContactsUsageDescription" = "سيقوم WinterGram برفع جهات الاتصال الخاصة بك باستمرار إلى خوادم التخزين السحابية ذات التشفير العالي لتتمكن من التواصل مع أصدقائك من خلال جميع أجهزتك.";
+"NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، WinterGram يحتاج لصلاحيات لعرض الخريطة لهم.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "عندما تختار أن تشارك مكانك بشكل حي مع أصدقائك في المحادثة، يحتاج WinterGram إلى الوصول لموقعك في الخلفية حتى بعد إغلاق WinterGram خلال فترة المشاركة.";
+"NSLocationAlwaysUsageDescription" = "عندما تقوم بمشاركة موقعك مع أصدقائك، WinterGram يحتاج إلى الصلاحية ليعرض لهم الخريطة. كما تحتاج لإعطاء WinterGram الصلاحية لتتمكن من إرسال موقعك من ساعة آبل.";
"NSCameraUsageDescription" = "نحتاج ذلك لتتمكن من التقاط وإرسال الصور والفيديوهات.";
"NSPhotoLibraryUsageDescription" = "نحتاج ذلك لتتمكن من إرسال الصور والفيديوهات من ألبوم الصور.";
"NSPhotoLibraryAddUsageDescription" = "نحتاج هذه الصلاحية لتتمكن من حفظ وسائطك في مكتبة الصور الخاصة بك.";
diff --git a/Telegram/Telegram-iOS/be.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
index 3f3e41d337..1e81b044af 100644
--- a/Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram будзе запампоўваць вашы кантакты на свае моцна абароненыя і зашыфраваныя воблачныя серверы, каб вы маглі кантактаваць са сваімі сябрамі з любой вашай прылады.";
-"NSLocationWhenInUseUsageDescription" = "Калі вы адпраўляе месцазнаходжанне сваім сябрам, Telegram патрэбны доступ да сэрвісаў геалакацыі, каб размясціць вас на карце.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі.";
-"NSLocationAlwaysUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі. Гэта таксама неабходна для таго, каб адпраўляць месцазнаходжанне праз Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram будзе запампоўваць вашы кантакты на свае моцна абароненыя і зашыфраваныя воблачныя серверы, каб вы маглі кантактаваць са сваімі сябрамі з любой вашай прылады.";
+"NSLocationWhenInUseUsageDescription" = "Калі вы адпраўляе месцазнаходжанне сваім сябрам, WinterGram патрэбны доступ да сэрвісаў геалакацыі, каб размясціць вас на карце.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, WinterGram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі.";
+"NSLocationAlwaysUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, WinterGram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі. Гэта таксама неабходна для таго, каб адпраўляць месцазнаходжанне праз Apple Watch.";
"NSCameraUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі рабіць і адпраўляць фота і відэа, а таксама відэавыклікі.";
"NSPhotoLibraryUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі абагульваць фота і відэа са сваёй галерэі.";
"NSPhotoLibraryAddUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі захоўваць фота і відэа ў сваю галерэю.";
diff --git a/Telegram/Telegram-iOS/ca.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ca.lproj/InfoPlist.strings
index f57e0cecfb..2ab5c31adf 100644
--- a/Telegram/Telegram-iOS/ca.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/ca.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram pujarà automàticament els vostres contactes als seus servidors xifrats, perquè pugueu connectar-vos amb els amics des de qualsevol dispositiu.";
-"NSLocationWhenInUseUsageDescription" = "Si envieu la vostra ubicació als amics, Telegram requereix accés per a mostra-los un mapa.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, Telegram requereix accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe.";
-"NSLocationAlwaysUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, Telegram requereix de tenir accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe. També necessiteu això per a enviar ubicacions des d'un Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram pujarà automàticament els vostres contactes als seus servidors xifrats, perquè pugueu connectar-vos amb els amics des de qualsevol dispositiu.";
+"NSLocationWhenInUseUsageDescription" = "Si envieu la vostra ubicació als amics, WinterGram requereix accés per a mostra-los un mapa.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, WinterGram requereix accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe.";
+"NSLocationAlwaysUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, WinterGram requereix de tenir accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe. També necessiteu això per a enviar ubicacions des d'un Apple Watch.";
"NSCameraUsageDescription" = "Ens cal això perquè pugueu fer i compartir fotos i vídeos, així com fer videotrucades.";
"NSPhotoLibraryUsageDescription" = "Ens cal això perquè pugueu compartir fotos i vídeos de la biblioteca de fotos.";
"NSPhotoLibraryAddUsageDescription" = "Ens cal això perquè així pugueu desar fotos i vídeos a la biblioteca de fotos.";
diff --git a/Telegram/Telegram-iOS/de.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/de.lproj/InfoPlist.strings
index 792eaa3cc3..c7bbf7bbb7 100644
--- a/Telegram/Telegram-iOS/de.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/de.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram lädt deine Kontakte durchgehend auf die stark verschlüsselten Cloud Server, damit du dich mit deinen Freunden auf all deinen Geräten verbinden kannst.";
-"NSLocationWhenInUseUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Wenn du deinen Live-Standort mit Freunden im Chat teilen möchtest, benötigt Telegram so lange im Hintergrund Zugriff auf deinen Standort, bis du ihn nicht mehr teilen willst.";
-"NSLocationAlwaysUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben. Diese Bereichtigung wird auch für die Apple Watch benötigt.";
+"NSContactsUsageDescription" = "WinterGram lädt deine Kontakte durchgehend auf die stark verschlüsselten Cloud Server, damit du dich mit deinen Freunden auf all deinen Geräten verbinden kannst.";
+"NSLocationWhenInUseUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du WinterGram den Zugriff darauf erlauben.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Wenn du deinen Live-Standort mit Freunden im Chat teilen möchtest, benötigt WinterGram so lange im Hintergrund Zugriff auf deinen Standort, bis du ihn nicht mehr teilen willst.";
+"NSLocationAlwaysUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du WinterGram den Zugriff darauf erlauben. Diese Bereichtigung wird auch für die Apple Watch benötigt.";
"NSCameraUsageDescription" = "Brauchen wir, damit du Bilder und Videos aufnehmen und teilen kannst.";
"NSPhotoLibraryUsageDescription" = "Brauchen wir, damit du Bilder und Videos aus deiner Fotomediathek teilen kannst.";
"NSPhotoLibraryAddUsageDescription" = "Brauchen wir, damit du Bilder und Videos in deiner Fotomediathek speichern kannst.";
diff --git a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist
index 504ece4483..de4e438bc4 100644
--- a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist
+++ b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist
@@ -9,7 +9,7 @@
INSendMessageIntent
IntentExamples
- Send a Telegram message to Alex saying I'll be there in 10 minutes
+ Send a WinterGram message to Alex saying I'll be there in 10 minutes
diff --git a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings
index e77338c5d5..ba2e3ce37c 100644
--- a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
-"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing.";
-"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
+"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, WinterGram needs access to show them a map.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, WinterGram needs background access to your location to keep them updated for the duration of the live sharing.";
+"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, WinterGram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch.";
"NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls.";
"NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library and save the ones you capture.";
"NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library and save the ones you capture.";
diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings
index 52ad3e640f..9a92601e2f 100644
--- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings
+++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings
@@ -50,7 +50,7 @@
"PUSH_MESSAGE_STICKER" = "%1$@|sent you a %2$@sticker";
"PUSH_CHAT_MESSAGE_STICKER" = "%2$@|%1$@ sent a %3$@sticker";
-"PUSH_CONTACT_JOINED" = "%1$@|joined Telegram!";
+"PUSH_CONTACT_JOINED" = "%1$@|joined WinterGram!";
"PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@";
"PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message";
@@ -205,7 +205,7 @@
"PUSH_PINNED_GIF" = "%1$@|pinned a GIF";
"PUSH_PINNED_PAID_MEDIA" = "%1$@|pinned a paid post for %2$@";
-"PUSH_CONTACT_JOINED" = "%1$@|joined Telegram!";
+"PUSH_CONTACT_JOINED" = "%1$@|joined WinterGram!";
"PUSH_AUTH_UNKNOWN" = "New login|from unrecognized device %1$@";
"PUSH_AUTH_REGION" = "New login|from unrecognized device %1$@, location: %2$@";
@@ -401,38 +401,38 @@
"Date.ChatDateHeaderYear" = "%1$@ %2$@, %3$@";
// Tour
-"Tour.Title1" = "Telegram";
+"Tour.Title1" = "WinterGram";
"Tour.Text1" = "The world's **fastest** messaging app.\nIt is **free** and **secure**.";
"Tour.Title2" = "Fast";
-"Tour.Text2" = "**Telegram** delivers messages\nfaster than any other application.";
+"Tour.Text2" = "**WinterGram** delivers messages\nfaster than any other application.";
"Tour.Title3" = "Powerful";
-"Tour.Text3" = "**Telegram** has no limits on\nthe size of your media and chats.";
+"Tour.Text3" = "**WinterGram** has no limits on\nthe size of your media and chats.";
"Tour.Title4" = "Secure";
-"Tour.Text4" = "**Telegram** keeps your messages\nsafe from hacker attacks.";
+"Tour.Text4" = "**WinterGram** keeps your messages\nsafe from hacker attacks.";
"Tour.Title5" = "Cloud-Based";
-"Tour.Text5" = "**Telegram** lets you access your\nmessages from multiple devices.";
+"Tour.Text5" = "**WinterGram** lets you access your\nmessages from multiple devices.";
"Tour.Title6" = "Free";
-"Tour.Text6" = "**Telegram** provides free unlimited\ncloud storage for chats and media.";
+"Tour.Text6" = "**WinterGram** provides free unlimited\ncloud storage for chats and media.";
"Tour.StartButton" = "Start Messaging";
// Login
"Login.PhoneAndCountryHelp" = "Please confirm your country code\nand enter your phone number.";
-"Login.CodeSentInternal" = "We've sent the code to the **Telegram** app on your other device";
+"Login.CodeSentInternal" = "We've sent the code to the **WinterGram** app on your other device";
"Login.HaveNotReceivedCodeInternal" = "Didn't get the code?";
"Login.CodeSentSms" = "We've sent you an SMS with the code";
"Login.Code" = "Code";
-"Login.WillCallYou" = "Telegram will call you in %@";
-"Login.CallRequestState2" = "Requesting a call from Telegram…";
-"Login.CallRequestState3" = "Telegram dialed your number\n[Didn't get the code?]";
+"Login.WillCallYou" = "WinterGram will call you in %@";
+"Login.CallRequestState2" = "Requesting a call from WinterGram…";
+"Login.CallRequestState3" = "WinterGram dialed your number\n[Didn't get the code?]";
"Login.EmailNotConfiguredError" = "An email account is required so that you can send us details about the error.\n\nPlease go to your device‘s settings > Passwords & Accounts > Add account and set up an email account.";
"Login.EmailCodeSubject" = "%@, no code";
-"Login.EmailCodeBody" = "My phone number is:\n%@\nI can't get an activation code for Telegram.";
+"Login.EmailCodeBody" = "My phone number is:\n%@\nI can't get an activation code for WinterGram.";
"Login.UnknownError" = "An error occurred, please try again later.";
"Login.InvalidCodeError" = "Invalid code, please try again.";
"Login.NetworkError" = "Please check your internet connection and try again.";
@@ -443,13 +443,13 @@
"Login.InvalidLastNameError" = "Sorry, this last name can't be used.";
"Login.InvalidPhoneEmailSubject" = "Invalid phone number: %@";
-"Login.InvalidPhoneEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram says it's invalid. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@";
+"Login.InvalidPhoneEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut WinterGram says it's invalid. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@";
"Login.PhoneBannedEmailSubject" = "Banned phone number: %@";
-"Login.PhoneBannedEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram says it's banned. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@";
+"Login.PhoneBannedEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut WinterGram says it's banned. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@";
-"Login.PhoneGenericEmailSubject" = "Telegram iOS error: %@";
-"Login.PhoneGenericEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram shows an error. Please help.\n\nError: %2$@\nApp version: %3$@\nOS version: %4$@\nLocale: %5$@\nMNC: %6$@";
+"Login.PhoneGenericEmailSubject" = "WinterGram iOS error: %@";
+"Login.PhoneGenericEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut WinterGram shows an error. Please help.\n\nError: %2$@\nApp version: %3$@\nOS version: %4$@\nLocale: %5$@\nMNC: %6$@";
"Login.PhonePaidEmailSubject" = "Payment Issue";
@@ -492,11 +492,11 @@
"Contacts.TabTitle" = "Contacts";
"Contacts.Title" = "Contacts";
"Contacts.FailedToSendInvitesMessage" = "An error occurred.";
-"Contacts.AccessDeniedError" = "Telegram does not have access to your contacts";
-"Contacts.AccessDeniedHelpLandscape" = "Please go to your %@ Settings — Privacy — Contacts.\nThen select ON for Telegram.";
-"Contacts.AccessDeniedHelpPortrait" = "Please go to your %@ Settings — Privacy — Contacts. Then select ON for Telegram.";
+"Contacts.AccessDeniedError" = "WinterGram does not have access to your contacts";
+"Contacts.AccessDeniedHelpLandscape" = "Please go to your %@ Settings — Privacy — Contacts.\nThen select ON for WinterGram.";
+"Contacts.AccessDeniedHelpPortrait" = "Please go to your %@ Settings — Privacy — Contacts. Then select ON for WinterGram.";
"Contacts.AccessDeniedHelpON" = "ON";
-"Contacts.InviteToTelegram" = "Invite to Telegram";
+"Contacts.InviteToTelegram" = "Invite to WinterGram";
"Contacts.InviteFriends" = "Invite Friends";
"Contacts.SelectAll" = "Select All";
@@ -532,7 +532,7 @@
"Conversation.Contact" = "Contact";
"Conversation.BlockUser" = "Block User";
"Conversation.UnblockUser" = "Unblock User";
-"Conversation.UnsupportedMedia" = "This message is not supported on your version of Telegram. Update the app to view:\nhttps://telegram.org/update";
+"Conversation.UnsupportedMedia" = "This message is not supported on your version of WinterGram. Update the app to view:\nhttps://telegram.org/update";
"Conversation.EncryptionWaiting" = "Waiting for %@ to get online...";
"Conversation.EncryptionProcessing" = "Exchanging encryption keys...";
"Conversation.EmptyPlaceholder" = "No messages here yet...";
@@ -580,9 +580,9 @@
"Notification.CreatedChannel" = "Channel created";
"Notification.CreatedGroup" = "Group created";
"Notification.CreatedChatWithTitle" = "%@ created the group \"%@\" ";
-"Notification.Joined" = "%@ joined Telegram";
+"Notification.Joined" = "%@ joined WinterGram";
"Notification.ChangedGroupName" = "%@ changed group name to \"%@\" ";
-"Notification.NewAuthDetected" = "%1$@,\nWe detected a login into your account from a new device on %2$@, %3$@ at %4$@\n\nDevice: %5$@\nLocation: %6$@\n\nIf this wasn't you, you can go to Settings — Privacy and Security — Sessions and terminate that session.\n\nIf you think that somebody logged in to your account against your will, you can enable two-step verification in Privacy and Security settings.\n\nSincerely,\nThe Telegram Team";
+"Notification.NewAuthDetected" = "%1$@,\nWe detected a login into your account from a new device on %2$@, %3$@ at %4$@\n\nDevice: %5$@\nLocation: %6$@\n\nIf this wasn't you, you can go to Settings — Privacy and Security — Sessions and terminate that session.\n\nIf you think that somebody logged in to your account against your will, you can enable two-step verification in Privacy and Security settings.\n\nSincerely,\nThe WinterGram Team";
"Notification.MessageLifetimeChanged" = "%1$@ set the self-destruct timer to %2$@";
"Notification.MessageLifetimeChangedOutgoing" = "You set the self-destruct timer to %1$@";
"Notification.MessageLifetimeRemoved" = "%1$@ disabled the self-destruct timer";
@@ -647,7 +647,7 @@
// User Profile
"Profile.CreateEncryptedChatError" = "An error occurred.";
-"Profile.CreateEncryptedChatOutdatedError" = "Cannot create a secret chat with %@.\n%@ is using an older version of Telegram and needs to update first.";
+"Profile.CreateEncryptedChatOutdatedError" = "Cannot create a secret chat with %@.\n%@ is using an older version of WinterGram and needs to update first.";
"Profile.CreateNewContact" = "Create New Contact";
"Profile.AddToExisting" = "Add to Existing Contact";
"Profile.EncryptionKey" = "Encryption Key";
@@ -679,7 +679,7 @@
"UserInfo.NotificationsDisabled" = "Disabled";
"UserInfo.NotificationsEnable" = "Enable";
"UserInfo.NotificationsDisable" = "Disable";
-"UserInfo.Invite" = "Invite to Telegram";
+"UserInfo.Invite" = "Invite to WinterGram";
// New Contact
"NewContact.Title" = "New Contact";
@@ -753,9 +753,9 @@
"Settings.BlockedUsers" = "Blocked Users";
"Settings.ChatBackground" = "Chat Background";
"Settings.Support" = "Ask a Question";
-"Settings.FAQ" = "Telegram FAQ";
+"Settings.FAQ" = "WinterGram FAQ";
"Settings.FAQ_URL" = "https://telegram.org/faq#general";
-"Settings.FAQ_Intro" = "Please note that Telegram Support is done by volunteers. We try to respond as quickly as possible, but it may take a while.\n\nPlease take a look at the Telegram FAQ: it has important troubleshooting tips and answers to most questions.";
+"Settings.FAQ_Intro" = "Please note that WinterGram Support is done by volunteers. We try to respond as quickly as possible, but it may take a while.\n\nPlease take a look at the WinterGram FAQ: it has important troubleshooting tips and answers to most questions.";
"Settings.FAQ_Button" = "FAQ";
"Settings.SaveIncomingPhotos" = "Save Incoming Photos";
@@ -812,7 +812,7 @@
"Cache.Title" = "Storage Usage";
"Cache.ClearCache" = "Clear Cache";
"Cache.KeepMedia" = "Keep Media";
-"Cache.Help" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
+"Cache.Help" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the WinterGram cloud and can be re-downloaded if you need it again.";
// Blocked Users
"BlockedUsers.Title" = "Blocked";
@@ -836,9 +836,9 @@
"BroadcastListInfo.AddRecipient" = "Add Recipient";
"Settings.LogoutConfirmationTitle" = "Log out?";
-"Settings.LogoutConfirmationText" = "\nNote that you can seamlessly use Telegram on all your devices at once.\n\nRemember, logging out kills all your Secret Chats.";
+"Settings.LogoutConfirmationText" = "\nNote that you can seamlessly use WinterGram on all your devices at once.\n\nRemember, logging out kills all your Secret Chats.";
-"Login.PadPhoneHelp" = "\nYou can use your main mobile number to log in to Telegram on all devices.\nDon't use your iPad's SIM number here — we'll need to send you an SMS.\n\nIs this number correct?\n{number}";
+"Login.PadPhoneHelp" = "\nYou can use your main mobile number to log in to WinterGram on all devices.\nDon't use your iPad's SIM number here — we'll need to send you an SMS.\n\nIs this number correct?\n{number}";
"Login.PadPhoneHelpTitle" = "Your Number";
"MessageTimer.Custom" = "Custom";
@@ -926,7 +926,7 @@
"Activity.RecordingAudio" = "recording audio";
"Activity.RecordingVideoMessage" = "recording video";
-"Compatibility.SecretMediaVersionTooLow" = "%@ is using an older version of Telegram, so secret photos will be shown in compatibility mode.\n\nOnce %@ updates Telegram, photos with timers for 1 minute or less will start working in 'Tap and hold to view' mode, and you will be notified whenever the other party takes a screenshot.";
+"Compatibility.SecretMediaVersionTooLow" = "%@ is using an older version of WinterGram, so secret photos will be shown in compatibility mode.\n\nOnce %@ updates WinterGram, photos with timers for 1 minute or less will start working in 'Tap and hold to view' mode, and you will be notified whenever the other party takes a screenshot.";
"Contacts.GlobalSearch" = "Global Search";
"Profile.Username" = "username";
@@ -935,7 +935,7 @@
"Username.Title" = "Username";
"Username.Placeholder" = "Your Username";
-"Username.Help" = "You can choose a username on **Telegram**. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
+"Username.Help" = "You can choose a username on **WinterGram**. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
"Username.InvalidTooShort" = "A username must have at least 5 characters.";
"Username.InvalidStartsWithNumber" = "Sorry, a username can't start with a number.";
"Username.InvalidCharacters" = "Sorry, this username is invalid.";
@@ -1040,8 +1040,8 @@
"Settings.PhoneNumber" = "Change Number";
-"PhoneNumberHelp.Help" = "You can change your Telegram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\n**Important:** all your Telegram contacts will get your **new number** added to their address book, provided they had your old number and you haven't blocked them in Telegram.";
-"PhoneNumberHelp.Alert" = "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.";
+"PhoneNumberHelp.Help" = "You can change your WinterGram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\n**Important:** all your WinterGram contacts will get your **new number** added to their address book, provided they had your old number and you haven't blocked them in WinterGram.";
+"PhoneNumberHelp.Alert" = "All your WinterGram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in WinterGram.";
"PhoneNumberHelp.ChangeNumber" = "Change Number";
"ChangePhoneNumberNumber.Title" = "Change Number";
@@ -1052,9 +1052,9 @@
"ChangePhoneNumberCode.Code" = "YOUR CODE";
"ChangePhoneNumberCode.CodePlaceholder" = "Code";
"ChangePhoneNumberCode.Help" = "We have sent you an SMS with the code";
-"ChangePhoneNumberCode.CallTimer" = "Telegram will call you in %@";
-"ChangePhoneNumberCode.RequestingACall" = "Requesting a call from Telegram...";
-"ChangePhoneNumberCode.Called" = "Telegram dialed your number";
+"ChangePhoneNumberCode.CallTimer" = "WinterGram will call you in %@";
+"ChangePhoneNumberCode.RequestingACall" = "Requesting a call from WinterGram...";
+"ChangePhoneNumberCode.Called" = "WinterGram dialed your number";
"LoginPassword.Title" = "Your Password";
"LoginPassword.PasswordPlaceholder" = "Password";
@@ -1206,7 +1206,7 @@
"SharedMedia.EmptyFilesText" = "You can send and receive\nfiles of any type up to 1.5 GB each\nand access them anywhere.";
"ShareFileTip.Title" = "Sharing Files";
-"ShareFileTip.Text" = "You can share **uncompressed** media files from your Camera Roll here.\n\nTo share files of any other type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram.";
+"ShareFileTip.Text" = "You can share **uncompressed** media files from your Camera Roll here.\n\nTo share files of any other type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose WinterGram.";
"ShareFileTip.CloseTip" = "Close Tip";
"DialogList.SearchSectionDialogs" = "Chats and Contacts";
@@ -1214,7 +1214,7 @@
"DialogList.SearchSectionGlobal" = "Global Search";
"DialogList.SearchSectionMessages" = "Messages";
-"Username.LinkHint" = "This link opens a chat with you in Telegram:[\nhttps://t.me/%@]";
+"Username.LinkHint" = "This link opens a chat with you in WinterGram:[\nhttps://t.me/%@]";
"Username.LinkCopied" = "Copied link to clipboard";
"SharedMedia.DeleteItemsConfirmation_1" = "Delete media file?";
@@ -1234,18 +1234,18 @@
"PasscodeSettings.SimplePasscode" = "Simple Passcode";
"PasscodeSettings.SimplePasscodeHelp" = "A simple passcode is a 4 digit number.";
"PasscodeSettings.EncryptData" = "Encrypt Local Database";
-"PasscodeSettings.EncryptDataHelp" = "Experimental feature, use with caution. Encrypt your local Telegram data, using a derivative of your passcode as the key.";
+"PasscodeSettings.EncryptDataHelp" = "Experimental feature, use with caution. Encrypt your local WinterGram data, using a derivative of your passcode as the key.";
-"EnterPasscode.EnterTitle" = "Enter your Telegram Passcode";
+"EnterPasscode.EnterTitle" = "Enter your WinterGram Passcode";
"EnterPasscode.ChangeTitle" = "Change Passcode";
-"EnterPasscode.EnterPasscode" = "Enter your Telegram Passcode";
+"EnterPasscode.EnterPasscode" = "Enter your WinterGram Passcode";
"EnterPasscode.EnterNewPasscodeNew" = "Enter a passcode";
"EnterPasscode.EnterNewPasscodeChange" = "Enter your new passcode";
"EnterPasscode.RepeatNewPasscode" = "Re-enter your new passcode";
"EnterPasscode.EnterCurrentPasscode" = "Enter your current passcode";
-"EnterPasscode.TouchId" = "Unlock Telegram";
+"EnterPasscode.TouchId" = "Unlock WinterGram";
-"DialogList.PasscodeLockHelp" = "Tap to lock Telegram";
+"DialogList.PasscodeLockHelp" = "Tap to lock WinterGram";
"PasscodeSettings.AutoLock" = "Auto-Lock";
"PasscodeSettings.AutoLock.Disabled" = "Disabled";
@@ -1264,32 +1264,32 @@
"AccessDenied.Title" = "Please Allow Access";
-"AccessDenied.Contacts" = "Telegram messaging is based on your existing contact list.\n\nPlease go to Settings > Privacy > Contacts and set Telegram to ON.";
+"AccessDenied.Contacts" = "WinterGram messaging is based on your existing contact list.\n\nPlease go to Settings > Privacy > Contacts and set WinterGram to ON.";
-"AccessDenied.VoiceMicrophone" = "Telegram needs access to your microphone to send voice messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON.";
+"AccessDenied.VoiceMicrophone" = "WinterGram needs access to your microphone to send voice messages.\n\nPlease go to Settings > Privacy > Microphone and set WinterGram to ON.";
-"AccessDenied.VideoMicrophone" = "Telegram needs access to your microphone to record sound in videos recording.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON.";
+"AccessDenied.VideoMicrophone" = "WinterGram needs access to your microphone to record sound in videos recording.\n\nPlease go to Settings > Privacy > Microphone and set WinterGram to ON.";
-"AccessDenied.MicrophoneRestricted" = "Microphone access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Microphone and set Telegram to ON.";
+"AccessDenied.MicrophoneRestricted" = "Microphone access is restricted for WinterGram.\n\nPlease go to Settings > General > Restrictions > Microphone and set WinterGram to ON.";
-"AccessDenied.Camera" = "Telegram needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
+"AccessDenied.Camera" = "WinterGram needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set WinterGram to ON.";
-"AccessDenied.CameraRestricted" = "Camera access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Camera and set Telegram to ON.";
+"AccessDenied.CameraRestricted" = "Camera access is restricted for WinterGram.\n\nPlease go to Settings > General > Restrictions > Camera and set WinterGram to ON.";
"AccessDenied.CameraDisabled" = "Camera access is globally restricted on your phone.\n\nPlease go to Settings > General > Restrictions and set Camera to ON";
-"AccessDenied.PhotosAndVideos" = "Telegram needs access to your photo library to send photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON.";
+"AccessDenied.PhotosAndVideos" = "WinterGram needs access to your photo library to send photos and videos.\n\nPlease go to Settings > Privacy > Photos and set WinterGram to ON.";
-"AccessDenied.SaveMedia" = "Telegram needs access to your photo library to save photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON.";
+"AccessDenied.SaveMedia" = "WinterGram needs access to your photo library to save photos and videos.\n\nPlease go to Settings > Privacy > Photos and set WinterGram to ON.";
-"AccessDenied.PhotosRestricted" = "Photo access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Photos and set Telegram to ON.";
+"AccessDenied.PhotosRestricted" = "Photo access is restricted for WinterGram.\n\nPlease go to Settings > General > Restrictions > Photos and set WinterGram to ON.";
-"AccessDenied.LocationDenied" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON.";
+"AccessDenied.LocationDenied" = "WinterGram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set WinterGram to ON.";
-"AccessDenied.LocationDisabled" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
+"AccessDenied.LocationDisabled" = "WinterGram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
-"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
+"AccessDenied.LocationTracking" = "WinterGram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
"AccessDenied.Settings" = "Settings";
@@ -1378,7 +1378,7 @@
"TwoStepAuth.SetupEmail" = "Set Recovery E-Mail";
"TwoStepAuth.ChangeEmail" = "Change Recovery E-Mail";
"TwoStepAuth.PendingEmailHelp" = "Your recovery e-mail %@ is not yet active and pending confirmation.";
-"TwoStepAuth.GenericHelp" = "You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your Telegram account.";
+"TwoStepAuth.GenericHelp" = "You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your WinterGram account.";
"TwoStepAuth.ConfirmationTitle" = "Two-Step Verification";
"TwoStepAuth.ConfirmationText" = "Please check your e-mail and click on the validation link to complete Two-Step Verification setup. Be sure to check the spam folder as well.";
@@ -1398,7 +1398,7 @@
"TwoStepAuth.EmailTitle" = "Recovery E-Mail";
"TwoStepAuth.EmailSkip" = "Skip";
-"TwoStepAuth.EmailSkipAlert" = "No, seriously.\n\nIf you forget your password, you will lose access to your Telegram account. There will be no way to restore it.";
+"TwoStepAuth.EmailSkipAlert" = "No, seriously.\n\nIf you forget your password, you will lose access to your WinterGram account. There will be no way to restore it.";
"TwoStepAuth.Email" = "E-Mail";
"TwoStepAuth.EmailPlaceholder" = "Your E-Mail";
"TwoStepAuth.EmailHelp" = "Please add your valid e-mail. It is the only way to recover a forgotten password.";
@@ -1427,7 +1427,7 @@
"Conversation.FileDropbox" = "Dropbox";
"Conversation.FileOpenIn" = "Open in...";
-"Conversation.FileHowToText" = "To share files of any type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram.";
+"Conversation.FileHowToText" = "To share files of any type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose WinterGram.";
"Map.LocationTitle" = "Location";
"Map.OpenInMaps" = "Open in Maps";
@@ -1461,9 +1461,9 @@
"Map.ETAHours_any" = "%@ h";
"Map.ETAHours_many" = "%@ h";
-"ChangePhone.ErrorOccupied" = "The number %@ is already connected to a Telegram account. Please delete that account before migrating to the new number.";
+"ChangePhone.ErrorOccupied" = "The number %@ is already connected to a WinterGram account. Please delete that account before migrating to the new number.";
-"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
+"AccessDenied.LocationTracking" = "WinterGram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON.";
"PrivacySettings.AuthSessions" = "Active Sessions";
"AuthSessions.Title" = "Active Sessions";
@@ -1473,7 +1473,7 @@
"AuthSessions.TerminateSession" = "Terminate session";
"AuthSessions.OtherSessions" = "ACTIVE SESSIONS";
"AuthSessions.EmptyTitle" = "No other sessions";
-"AuthSessions.EmptyText" = "You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized.";
+"AuthSessions.EmptyText" = "You can log in to WinterGram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized.";
"AuthSessions.AppUnofficial" = "(ID: %@)";
"WebPreview.GettingLinkInfo" = "Getting Link Info...";
@@ -1486,7 +1486,7 @@
"GroupInfo.InviteLink.Title" = "Invite Link";
"GroupInfo.InviteLink.LinkSection" = "LINK";
-"GroupInfo.InviteLink.Help" = "Anyone who has Telegram installed will be able to join your group by following this link.";
+"GroupInfo.InviteLink.Help" = "Anyone who has WinterGram installed will be able to join your group by following this link.";
"GroupInfo.InviteLink.CopyLink" = "Copy Link";
"GroupInfo.InviteLink.RevokeLink" = "Revoke Link";
"GroupInfo.InviteLink.ShareLink" = "Share Link";
@@ -1573,7 +1573,7 @@
"Login.PhoneNumberHelp" = "Help";
"Login.EmailPhoneSubject" = "Invalid number %@";
-"Login.EmailPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's invalid. Please help.\nAdditional Info: %@, %@.";
+"Login.EmailPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut WinterGram says it's invalid. Please help.\nAdditional Info: %@, %@.";
"SharedMedia.TitleLink" = "Shared Links";
"SharedMedia.EmptyLinksText" = "All links shared in this chat will appear here.";
@@ -1600,8 +1600,8 @@
"Channel.Edit.LinkItem" = "Link";
"Channel.Username.Title" = "Link";
-"Channel.Username.Help" = "You can choose a channel name on **Telegram**. If you do, other people will be able to find your channel by this name.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
-"Channel.Username.LinkHint" = "This link opens your channel in Telegram:[\nhttps://t.me/%@]";
+"Channel.Username.Help" = "You can choose a channel name on **WinterGram**. If you do, other people will be able to find your channel by this name.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
+"Channel.Username.LinkHint" = "This link opens your channel in WinterGram:[\nhttps://t.me/%@]";
"Channel.Username.InvalidTooShort" = "Channel names must have at least 5 characters.";
"Channel.Username.InvalidStartsWithNumber" = "Channel names can't start with a number.";
"Channel.Username.InvalidCharacters" = "Sorry, this name is invalid.";
@@ -1646,7 +1646,7 @@
"Channel.Setup.Title" = "Channel";
-"Channel.Username.CreatePublicLinkHelp" = "People can share this link with others and find your channel using Telegram search.";
+"Channel.Username.CreatePublicLinkHelp" = "People can share this link with others and find your channel using WinterGram search.";
"Channel.Username.CreatePrivateLinkHelp" = "People can join your channel by following this link. You can revoke the link at any time.";
"Channel.Setup.PublicNoLink" = "Please choose a link for your public channel, so that people can find it in search and share with others.\n\nIf you're not interested, we suggest creating a private channel instead.";
@@ -1772,7 +1772,7 @@
"Group.Management.AddModeratorHelp" = "You can add admins to help you manage your group.";
-"Watch.AppName" = "Telegram";
+"Watch.AppName" = "WinterGram";
"Watch.Compose.AddContact" = "Choose Contact";
"Watch.Compose.CreateMessage" = "Create Message";
"Watch.Compose.CurrentLocation" = "Current Location";
@@ -1807,7 +1807,7 @@
"Watch.Message.ForwardedFrom" = "Forwarded from";
-"Watch.Notification.Joined" = "Joined Telegram";
+"Watch.Notification.Joined" = "Joined WinterGram";
"Watch.MessageView.Title" = "Message";
"Watch.MessageView.Forward" = "Forward";
@@ -1821,9 +1821,9 @@
"Watch.Stickers.StickerPacks" = "Sticker Sets";
"Watch.Location.Current" = "Current Location";
-"Watch.Location.Access" = "Allow Telegram to access location on your phone";
+"Watch.Location.Access" = "Allow WinterGram to access location on your phone";
-"Watch.AuthRequired" = "Log in to Telegram on your phone to get started";
+"Watch.AuthRequired" = "Log in to WinterGram on your phone to get started";
"Watch.NoConnection" = "No Connection";
"Watch.ConnectionDescription" = "Your Watch needs to be connected for the app to work";
@@ -1870,7 +1870,7 @@
"Cache.ClearProgress" = "Please Wait...";
"Cache.ClearEmpty" = "Empty";
"Cache.ByPeerHeader" = "CHATS";
-"Cache.Indexing" = "Telegram is calculating current cache size.\nThis can take a few minutes.";
+"Cache.Indexing" = "WinterGram is calculating current cache size.\nThis can take a few minutes.";
"ExplicitContent.AlertTitle" = "Sorry";
"ExplicitContent.AlertChannel" = "You can't access this channel because it violates App Store rules.";
@@ -1951,7 +1951,7 @@
"PrivacyLastSeenSettings.GroupsAndChannelsHelp" = "Change who can add you to groups and channels.";
"MusicPlayer.VoiceNote" = "Voice Message";
-"Watch.Microphone.Access" = "Allow Telegram to access the microphone on your phone";
+"Watch.Microphone.Access" = "Allow WinterGram to access the microphone on your phone";
"Settings.AppleWatch" = "Apple Watch";
"AppleWatch.Title" = "Apple Watch";
@@ -1970,7 +1970,7 @@
"KeyCommand.SendMessage" = "Send Message";
"KeyCommand.ChatInfo" = "Chat Info";
-"Conversation.SecretLinkPreviewAlert" = "Would you like to enable extended link previews in Secret Chats? Note that link previews are generated on Telegram servers.";
+"Conversation.SecretLinkPreviewAlert" = "Would you like to enable extended link previews in Secret Chats? Note that link previews are generated on WinterGram servers.";
"Conversation.SecretChatContextBotAlert" = "Please note that inline bots are provided by third-party developers. For the bot to work, the symbols you type after the bot's username are sent to the respective developer.";
"Map.OpenInWaze" = "Open in Waze";
@@ -2044,7 +2044,7 @@
"Group.Setup.TypePublicHelp" = "Public groups can be found in search, chat history is available to everyone and anyone can join.";
"Group.Setup.TypePrivateHelp" = "Private groups can only be joined if you were invited or have an invite link.";
-"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using Telegram search.";
+"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using WinterGram search.";
"Group.Username.CreatePrivateLinkHelp" = "People can join your group by following this link. You can revoke the link at any time.";
"Conversation.PinMessageAlertGroup" = "Pin this message and notify all members of the group?";
@@ -2148,11 +2148,11 @@
"Login.CodeSentCall" = "We are calling your phone to dictate a code.";
"Login.WillSendSms" = "You can request an SMS in %@";
-"Login.SmsRequestState2" = "Requesting an SMS from Telegram...";
-"Login.SmsRequestState3" = "Telegram sent you an SMS\n[Didn't get the code?]";
+"Login.SmsRequestState2" = "Requesting an SMS from WinterGram...";
+"Login.SmsRequestState3" = "WinterGram sent you an SMS\n[Didn't get the code?]";
"CancelResetAccount.Title" = "Cancel Account Reset";
-"CancelResetAccount.TextSMS" = "Somebody with access to your phone number %@ has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf it wasn't you, please enter the code we've just sent you via SMS to your number.";
+"CancelResetAccount.TextSMS" = "Somebody with access to your phone number %@ has requested to delete your WinterGram account and reset your 2-Step Verification password.\n\nIf it wasn't you, please enter the code we've just sent you via SMS to your number.";
"CancelResetAccount.Success" = "The deletion process was cancelled for your account %@.";
"MediaPicker.MomentsDateRangeSameMonthYearFormat" = "{month} {day1} – {day2}, {year}";
@@ -2251,7 +2251,7 @@
"StickerPack.RemoveMaskCount_many" = "Remove %@ Masks";
"StickerPack.RemoveMaskCount_0" = "Remove %@ Masks";
-"Conversation.BotInteractiveUrlAlert" = "Allow %@ to pass your Telegram name and id (not your phone number) to pages you open with this bot?";
+"Conversation.BotInteractiveUrlAlert" = "Allow %@ to pass your WinterGram name and id (not your phone number) to pages you open with this bot?";
"StickerPacksSettings.ArchivedMasks" = "Archived Masks";
"StickerSettings.MaskContextInfo" = "If you archive a set of masks, you can quickly restore it later from the Archived Masks section.";
"StickerPacksSettings.ArchivedMasks.Info" = "You can have up to 200 sets of masks.
@@ -2259,9 +2259,9 @@ Unused sets are archived when you add more.";
"CloudStorage.Title" = "Cloud Storage";
-"Widget.AuthRequired" = "Log in to Telegram";
+"Widget.AuthRequired" = "Log in to WinterGram";
"Widget.NoUsers" = "Start messaging to see your friends here";
-"Widget.GalleryTitle" = "Telegram";
+"Widget.GalleryTitle" = "WinterGram";
"Widget.GalleryDescription" = "Select chats";
"ShareMenu.CopyShareLinkGame" = "Copy link to game";
@@ -2286,11 +2286,11 @@ Unused sets are archived when you add more.";
"Conversation.JumpToDate" = "Jump To Date";
"Conversation.AddToReadingList" = "Add to Reading List";
-"AccessDenied.CallMicrophone" = "Telegram needs access to your microphone for voice calls.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON.";
+"AccessDenied.CallMicrophone" = "WinterGram needs access to your microphone for voice calls.\n\nPlease go to Settings > Privacy > Microphone and set WinterGram to ON.";
"Call.EncryptionKey.Title" = "Encryption Key";
-"Application.Name" = "Telegram";
+"Application.Name" = "WinterGram";
"DialogList.Pin" = "Pin";
"DialogList.Unpin" = "Unpin";
"DialogList.PinLimitError" = "Sorry, you can pin no more than %@ chats to the top.";
@@ -2340,7 +2340,7 @@ Unused sets are archived when you add more.";
"Calls.AddTab" = "Add Tab";
"Calls.NewCall" = "New Call";
-"Calls.RatingTitle" = "Please rate the quality\nof your Telegram call";
+"Calls.RatingTitle" = "Please rate the quality\nof your WinterGram call";
"Calls.SubmitRating" = "Submit";
"Call.Seconds_1" = "%@ second";
@@ -2492,10 +2492,10 @@ Unused sets are archived when you add more.";
"Your_card_was_declined" = "Your card was declined.";
/* Error when the card's expiration month is not valid */
-"Your_cards_expiration_month_is_invalid" ="You've entered an invalid expiration month.";
+"Your_cards_expiration_month_is_invalid" = "You've entered an invalid expiration month.";
/* Error when the card's expiration year is not valid */
-"Your_cards_expiration_year_is_invalid" ="You've entered an invalid expiration year.";
+"Your_cards_expiration_year_is_invalid" = "You've entered an invalid expiration year.";
/* Error when the card number is not valid */
"Your_cards_number_is_invalid" = "You've entered an invalid card number.";
@@ -2527,14 +2527,14 @@ Unused sets are archived when you add more.";
"Calls.RatingFeedback" = "Write a comment...";
-"Call.StatusIncoming" = "Telegram Audio...";
+"Call.StatusIncoming" = "WinterGram Audio...";
"Call.IncomingVoiceCall" = "Incoming Voice Call";
"Call.IncomingVideoCall" = "Incoming Video Call";
"Call.StatusRequesting" = "Contacting...";
"Call.StatusWaiting" = "Waiting...";
"Call.StatusRinging" = "Ringing...";
"Call.StatusConnecting" = "Connecting...";
-"Call.StatusOngoing" = "Telegram Audio %@";
+"Call.StatusOngoing" = "WinterGram Audio %@";
"Call.StatusEnded" = "Call Ended";
"Call.StatusFailed" = "Call Failed";
"Call.StatusBusy" = "Busy";
@@ -2599,7 +2599,7 @@ Unused sets are archived when you add more.";
"Call.AudioRouteHeadphones" = "Headphones";
"Call.AudioRouteHide" = "Hide";
-"Call.PhoneCallInProgressMessage" = "You can’t place a Telegram call if you’re already on a phone call.";
+"Call.PhoneCallInProgressMessage" = "You can’t place a WinterGram call if you’re already on a phone call.";
"Call.RecordingDisabledMessage" = "Please end your call before recording a voice message.";
"Call.EmojiDescription" = "If these emoji are the same on %@'s screen, this call is 100%% secure.";
@@ -2609,8 +2609,8 @@ Unused sets are archived when you add more.";
"Conversation.HoldForAudio" = "Hold to record audio. Tap to switch to video.";
"Conversation.HoldForVideo" = "Hold to record video. Tap to switch to audio.";
-"UserInfo.TelegramCall" = "Telegram Call";
-"UserInfo.TelegramVideoCall" = "Telegram Video Call";
+"UserInfo.TelegramCall" = "WinterGram Call";
+"UserInfo.TelegramVideoCall" = "WinterGram Video Call";
"UserInfo.PhoneCall" = "Phone Call";
"SharedMedia.CategoryMedia" = "Media";
@@ -2618,8 +2618,8 @@ Unused sets are archived when you add more.";
"SharedMedia.CategoryLinks" = "Links";
"SharedMedia.CategoryOther" = "Audio";
-"AccessDenied.VideoMessageCamera" = "Telegram needs access to your camera to send video messages.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
-"AccessDenied.VideoMessageMicrophone" = "Telegram needs access to your microphone to send video messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON.";
+"AccessDenied.VideoMessageCamera" = "WinterGram needs access to your camera to send video messages.\n\nPlease go to Settings > Privacy > Camera and set WinterGram to ON.";
+"AccessDenied.VideoMessageMicrophone" = "WinterGram needs access to your microphone to send video messages.\n\nPlease go to Settings > Privacy > Microphone and set WinterGram to ON.";
"ChatSettings.AutomaticVideoMessageDownload" = "AUTOMATIC VIDEO MESSAGE DOWNLOAD";
@@ -2645,7 +2645,7 @@ Unused sets are archived when you add more.";
"Privacy.PaymentsTitle" = "PAYMENTS";
"Privacy.PaymentsClearInfo" = "Clear payment & shipping info";
-"Privacy.PaymentsClearInfoHelp" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data.";
+"Privacy.PaymentsClearInfoHelp" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that WinterGram never stores your credit card data.";
"Privacy.PaymentsClear.PaymentInfo" = "Payment Info";
"Privacy.PaymentsClear.ShippingInfo" = "Shipping Info";
@@ -2800,7 +2800,7 @@ Unused sets are archived when you add more.";
"Contacts.PhoneNumber" = "Phone Number";
"Contacts.AddPhoneNumber" = "Add %@";
-"Contacts.ShareTelegram" = "Share Telegram";
+"Contacts.ShareTelegram" = "Share WinterGram";
"Conversation.ViewChannel" = "VIEW CHANNEL";
"Conversation.ViewGroup" = "VIEW GROUP";
@@ -2864,10 +2864,10 @@ Unused sets are archived when you add more.";
"Group.Members.AddMemberBotErrorNotAllowed" = "Sorry, you don't have the necessary permissions to add bots to this group.";
"Privacy.Calls.P2P" = "Peer-to-Peer";
-"Privacy.Calls.P2PHelp" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but will slightly decrease audio quality.";
+"Privacy.Calls.P2PHelp" = "Disabling peer-to-peer will relay all calls through WinterGram servers to avoid revealing your IP address, but will slightly decrease audio quality.";
"Privacy.Calls.Integration" = "iOS Call Integration";
-"Privacy.Calls.IntegrationHelp" = "iOS Call Integration shows Telegram calls on the lock screen and in the system's call history. If iCloud sync is enabled, your call history is shared with Apple.";
+"Privacy.Calls.IntegrationHelp" = "iOS Call Integration shows WinterGram calls on the lock screen and in the system's call history. If iCloud sync is enabled, your call history is shared with Apple.";
"Call.ReportPlaceholder" = "What went wrong?";
"Call.ReportIncludeLog" = "Send technical information";
@@ -2894,12 +2894,12 @@ Unused sets are archived when you add more.";
"Stickers.FrequentlyUsed" = "Recently Used";
-"Contacts.ImportersCount_1" = "1 contact on Telegram";
-"Contacts.ImportersCount_2" = "2 contacts on Telegram";
-"Contacts.ImportersCount_3_10" = "%@ contacts on Telegram";
-"Contacts.ImportersCount_any" = "%@ contacts on Telegram";
-"Contacts.ImportersCount_many" = "%@ contacts on Telegram";
-"Contacts.ImportersCount_0" = "%@ contacts on Telegram";
+"Contacts.ImportersCount_1" = "1 contact on WinterGram";
+"Contacts.ImportersCount_2" = "2 contacts on WinterGram";
+"Contacts.ImportersCount_3_10" = "%@ contacts on WinterGram";
+"Contacts.ImportersCount_any" = "%@ contacts on WinterGram";
+"Contacts.ImportersCount_many" = "%@ contacts on WinterGram";
+"Contacts.ImportersCount_0" = "%@ contacts on WinterGram";
"Conversation.ContextMenuBan" = "Restrict";
@@ -2907,13 +2907,13 @@ Unused sets are archived when you add more.";
"SocksProxySetup.UseForCallsHelp" = "Proxy servers may degrade the quality of your calls.";
"InviteText.URL" = "https://telegram.org/dl";
-"InviteText.SingleContact" = "Hey, I'm using Telegram to chat. Join me! Download it here: %@";
-"InviteText.ContactsCountText_1" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}";
-"InviteText.ContactsCountText_2" = "Hey, I'm using Telegram to chat – and so are 2 of our other contacts. Join us! Download it here: {url}";
-"InviteText.ContactsCountText_3_10" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
-"InviteText.ContactsCountText_any" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
-"InviteText.ContactsCountText_many" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
-"InviteText.ContactsCountText_0" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}";
+"InviteText.SingleContact" = "Hey, I'm using WinterGram to chat. Join me! Download it here: %@";
+"InviteText.ContactsCountText_1" = "Hey, I'm using WinterGram to chat. Join me! Download it here: {url}";
+"InviteText.ContactsCountText_2" = "Hey, I'm using WinterGram to chat – and so are 2 of our other contacts. Join us! Download it here: {url}";
+"InviteText.ContactsCountText_3_10" = "Hey, I'm using WinterGram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
+"InviteText.ContactsCountText_any" = "Hey, I'm using WinterGram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
+"InviteText.ContactsCountText_many" = "Hey, I'm using WinterGram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}";
+"InviteText.ContactsCountText_0" = "Hey, I'm using WinterGram to chat. Join me! Download it here: {url}";
"Invite.LargeRecipientsCountWarning" = "Please note that it may take some time for your device to send all of these invitations";
@@ -3069,12 +3069,12 @@ Unused sets are archived when you add more.";
"NotificationSettings.ContactJoined" = "New Contacts";
-"AccessDenied.LocationAlwaysDenied" = "If you'd like to share your Live Location with friends, Telegram needs location access when the app is in the background.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to Always.";
+"AccessDenied.LocationAlwaysDenied" = "If you'd like to share your Live Location with friends, WinterGram needs location access when the app is in the background.\n\nPlease go to Settings > Privacy > Location Services and set WinterGram to Always.";
"UserInfo.UnblockConfirmation" = "Unblock %@?";
"Login.BannedPhoneSubject" = "Banned phone number: %@";
-"Login.BannedPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's banned. Please help.";
+"Login.BannedPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut WinterGram says it's banned. Please help.";
"Conversation.StopLiveLocation" = "Stop Sharing";
@@ -3145,7 +3145,7 @@ Unused sets are archived when you add more.";
"Channel.Setup.TypePrivateHelp" = "Private channels can only be joined if you were invited or have an invite link.";
"Group.Username.InvalidTooShort" = "Group names must have at least 5 characters.";
"Group.Username.InvalidStartsWithNumber" = "Group names can't start with a number.";
-"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using Telegram search.";
+"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using WinterGram search.";
"Channel.TypeSetup.Title" = "Channel Type";
"Group.Setup.TypePrivate" = "Private";
@@ -3157,16 +3157,16 @@ Unused sets are archived when you add more.";
"Privacy.PaymentsClearInfoDoneHelp" = "Payment & shipping info cleared.";
-"InfoPlist.NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
-"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map.";
+"InfoPlist.NSContactsUsageDescription" = "WinterGram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
+"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, WinterGram needs access to show them a map.";
"InfoPlist.NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls.";
"InfoPlist.NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library.";
"InfoPlist.NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library.";
"InfoPlist.NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound.";
"InfoPlist.NSSiriUsageDescription" = "You can use Siri to send messages.";
-"InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing.";
-"InfoPlist.NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch.";
-"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map.";
+"InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, WinterGram needs background access to your location to keep them updated for the duration of the live sharing.";
+"InfoPlist.NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, WinterGram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch.";
+"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, WinterGram needs access to show them a map.";
"InfoPlist.NSFaceIDUsageDescription" = "You can use Face ID to unlock the app.";
"Privacy.Calls.P2PNever" = "Never";
@@ -3210,10 +3210,10 @@ Unused sets are archived when you add more.";
"Channel.AdminLogFilter.EventsNewSubscribers" = "New Subscribers";
"Channel.AdminLogFilter.EventsLeavingSubscribers" = "Subscribers Removed";
-"Conversation.ClearPrivateHistory" = "This will delete all messages and media in this chat from your Telegram cloud. Your chat partner will still have them.";
-"Conversation.ClearGroupHistory" = "This will delete all messages and media in this chat from your Telegram cloud. Other members of the group will still have them.";
+"Conversation.ClearPrivateHistory" = "This will delete all messages and media in this chat from your WinterGram cloud. Your chat partner will still have them.";
+"Conversation.ClearGroupHistory" = "This will delete all messages and media in this chat from your WinterGram cloud. Other members of the group will still have them.";
"Conversation.ClearSecretHistory" = "This will delete all messages and media in this chat for both you and your chat partner.";
-"Conversation.ClearSelfHistory" = "This will delete all messages and media in this chat from your Telegram cloud.";
+"Conversation.ClearSelfHistory" = "This will delete all messages and media in this chat from your WinterGram cloud.";
"MediaPicker.LivePhotoDescription" = "The live photo will be sent as a GIF.";
@@ -3257,7 +3257,7 @@ Unused sets are archived when you add more.";
"AuthSessions.Sessions" = "Sessions";
"AuthSessions.LoggedIn" = "Websites";
"AuthSessions.LogOutApplications" = "Disconnect All Websites";
-"AuthSessions.LogOutApplicationsHelp" = "You can log in on websites that support signing in with Telegram.";
+"AuthSessions.LogOutApplicationsHelp" = "You can log in on websites that support signing in with WinterGram.";
"AuthSessions.LoggedInWithTelegram" = "CONNECTED WEBSITES";
"AuthSessions.LogOut" = "Disconnect";
"AuthSessions.Message" = "You allowed this bot to message you when you logged in on %@.";
@@ -3310,26 +3310,26 @@ Unused sets are archived when you add more.";
"DialogList.AdLabel" = "Proxy Sponsor";
"DialogList.AdNoticeAlert" = "The proxy you are using displays a sponsored channel in your chat list.";
-"SocksProxySetup.AdNoticeHelp" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal your Telegram traffic.";
+"SocksProxySetup.AdNoticeHelp" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal your WinterGram traffic.";
"SocksProxySetup.ShareProxyList" = "Share Proxy List";
"Privacy.SecretChatsTitle" = "SECRET CHATS";
"Privacy.SecretChatsLinkPreviews" = "Link Previews";
-"Privacy.SecretChatsLinkPreviewsHelp" = "Link previews will be generated on Telegram servers. We do not store data about the links you send.";
+"Privacy.SecretChatsLinkPreviewsHelp" = "Link previews will be generated on WinterGram servers. We do not store data about the links you send.";
"Privacy.ContactsTitle" = "CONTACTS";
"Privacy.ContactsSync" = "Sync Contacts";
"Privacy.ContactsSyncHelp" = "Turn on to continuously sync contacts from this device with your account.";
"Privacy.ContactsReset" = "Delete Synced Contacts";
-"Privacy.ContactsResetConfirmation" = "This will remove your contacts from the Telegram servers.\nIf 'Sync Contacts' is enabled, contacts will be re-synced.";
+"Privacy.ContactsResetConfirmation" = "This will remove your contacts from the WinterGram servers.\nIf 'Sync Contacts' is enabled, contacts will be re-synced.";
"Login.TermsOfServiceDecline" = "Decline";
"Login.TermsOfServiceAgree" = "Agree & Continue";
"Login.TermsOfService.ProceedBot" = "Please agree and proceed to %@.";
-"Login.TermsOfServiceSignupDecline" = "We're very sorry, but this means you can't sign up for Telegram.\n\nUnlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with Telegram's modest needs, it won't be possible for us to provide this service.";
+"Login.TermsOfServiceSignupDecline" = "We're very sorry, but this means you can't sign up for WinterGram.\n\nUnlike others, we don't use your data for ad targeting or other commercial purposes. WinterGram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with WinterGram's modest needs, it won't be possible for us to provide this service.";
"UserInfo.BotPrivacy" = "Privacy Policy";
@@ -3342,20 +3342,20 @@ Unused sets are archived when you add more.";
"PrivacyPolicy.AgeVerificationAgree" = "Agree";
"PrivacyPolicy.DeclineTitle" = "Decline";
-"PrivacyPolicy.DeclineMessage" = "We're very sorry, but this means we must part ways here. Unlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with Telegram's modest needs, it won't be possible for us to provide this service.";
+"PrivacyPolicy.DeclineMessage" = "We're very sorry, but this means we must part ways here. Unlike others, we don't use your data for ad targeting or other commercial purposes. WinterGram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with WinterGram's modest needs, it won't be possible for us to provide this service.";
"PrivacyPolicy.DeclineDeclineAndDelete" = "Decline and Delete";
-"PrivacyPolicy.DeclineLastWarning" = "Warning, this will irreversibly delete your Telegram account along with all the data you store in the Telegram cloud.\n\nWe will provide a tool to download your data before June, 23 – so you may want to wait a little before deleting.";
+"PrivacyPolicy.DeclineLastWarning" = "Warning, this will irreversibly delete your WinterGram account along with all the data you store in the WinterGram cloud.\n\nWe will provide a tool to download your data before June, 23 – so you may want to wait a little before deleting.";
"PrivacyPolicy.DeclineDeleteNow" = "Delete Now";
-"Settings.Passport" = "Telegram Passport";
+"Settings.Passport" = "WinterGram Passport";
"Passport.Title" = "Passport";
"Passport.RequestHeader" = "%@ requests access to your personal data to sign you up for their services.";
-"Passport.InfoTitle" = "What is Telegram Passport?";
-"Passport.InfoText" = "With **Telegram Passport** you can easily sign up for websites and services that require identity verification.\n\nYour information, personal data, and documents are protected by end-to-end encryption. Nobody, including Telegram, can access them without your permission.";
+"Passport.InfoTitle" = "What is WinterGram Passport?";
+"Passport.InfoText" = "With **WinterGram Passport** you can easily sign up for websites and services that require identity verification.\n\nYour information, personal data, and documents are protected by end-to-end encryption. Nobody, including WinterGram, can access them without your permission.";
"Passport.InfoLearnMore" = "Learn More";
"Passport.InfoFAQ_URL" = "https://telegram.org/faq#passport";
@@ -3376,19 +3376,19 @@ Unused sets are archived when you add more.";
"Passport.AcceptHelp" = "You are sending your documents directly to %1$@ and allowing their @%2$@ to send you messages.";
"Passport.Authorize" = "Authorize";
-"Passport.DeletePassport" = "Delete Telegram Passport";
-"Passport.DeletePassportConfirmation" = "Are you sure you want to delete your Telegram Passport? All details will be lost.";
+"Passport.DeletePassport" = "Delete WinterGram Passport";
+"Passport.DeletePassportConfirmation" = "Are you sure you want to delete your WinterGram Passport? All details will be lost.";
-"Passport.PasswordHelp" = "Please enter your Telegram Password\nto decrypt your data";
+"Passport.PasswordHelp" = "Please enter your WinterGram Password\nto decrypt your data";
"Passport.PasswordPlaceholder" = "Enter your password";
"Passport.InvalidPasswordError" = "Invalid password. Please try again.";
"Passport.FloodError" = "Limit exceeded. Please try again later.";
-"Passport.UpdateRequiredError" = "Sorry, your Telegram app is out of date and can’t handle this request. Please update Telegram.";
+"Passport.UpdateRequiredError" = "Sorry, your WinterGram app is out of date and can’t handle this request. Please update WinterGram.";
"Passport.ForgottenPassword" = "Forgotten Password";
-"Passport.PasswordReset" = "All documents uploaded to your Telegram Passport will be lost. You will be able to upload new documents.";
+"Passport.PasswordReset" = "All documents uploaded to your WinterGram Passport will be lost. You will be able to upload new documents.";
-"Passport.PasswordDescription" = "Please create a password to secure your personal data with end-to-end encryption.\n\nThis password will also be required whenever you log in to Telegram on a new device.";
+"Passport.PasswordDescription" = "Please create a password to secure your personal data with end-to-end encryption.\n\nThis password will also be required whenever you log in to WinterGram on a new device.";
"Passport.PasswordCreate" = "Create a Password";
"Passport.PasswordCompleteSetup" = "Complete Password Setup";
"Passport.PasswordNext" = "Next";
@@ -3508,14 +3508,14 @@ Unused sets are archived when you add more.";
"Passport.Phone.Title" = "Phone Number";
"Passport.Phone.UseTelegramNumber" = "Use %@";
-"Passport.Phone.UseTelegramNumberHelp" = "Use the same phone number as on Telegram.";
+"Passport.Phone.UseTelegramNumberHelp" = "Use the same phone number as on WinterGram.";
"Passport.Phone.EnterOtherNumber" = "OR ENTER NEW PHONE NUMBER";
"Passport.Phone.Help" = "Note: You will receive a confirmation code on the phone number you provide.";
"Passport.Phone.Delete" = "Delete Phone Number";
"Passport.Email.Title" = "Email";
"Passport.Email.UseTelegramEmail" = "Use %@";
-"Passport.Email.UseTelegramEmailHelp" = "Use the same address as on Telegram.";
+"Passport.Email.UseTelegramEmailHelp" = "Use the same address as on WinterGram.";
"Passport.Email.EnterOtherEmail" = "OR ENTER NEW EMAIL ADDRESS";
"Passport.Email.EmailPlaceholder" = "Enter your email address";
"Passport.Email.Help" = "Note: You will receive a confirmation code to the email address you provide.";
@@ -3541,7 +3541,7 @@ Unused sets are archived when you add more.";
"Passport.ScanPassport" = "Scan Your Passport";
"Passport.ScanPassportHelp" = "Scan your passport or identity card with machine-readable zone to fill personal details automatically.";
-"TwoStepAuth.PasswordRemovePassportConfirmation" = "Are you sure you want to disable your password?\n\nWarning! All data saved in your Telegram Passport will be lost!";
+"TwoStepAuth.PasswordRemovePassportConfirmation" = "Are you sure you want to disable your password?\n\nWarning! All data saved in your WinterGram Passport will be lost!";
"Application.Update" = "Update";
@@ -3586,17 +3586,17 @@ Unused sets are archived when you add more.";
"Passport.CorrectErrors" = "Tap to correct errors";
-"Passport.NotLoggedInMessage" = "Please log in to your account to use Telegram Passport";
+"Passport.NotLoggedInMessage" = "Please log in to your account to use WinterGram Passport";
-"Update.Title" = "Telegram Update";
-"Update.AppVersion" = "Telegram %@";
-"Update.UpdateApp" = "Update Telegram";
+"Update.Title" = "WinterGram Update";
+"Update.AppVersion" = "WinterGram %@";
+"Update.UpdateApp" = "Update WinterGram";
"Update.Skip" = "Skip";
"ReportPeer.ReasonCopyright" = "Copyright";
"PrivacySettings.DataSettings" = "Data Settings";
-"PrivacySettings.DataSettingsHelp" = "Control which of your data is stored in the cloud and used by Telegram to enable advanced features.";
+"PrivacySettings.DataSettingsHelp" = "Control which of your data is stored in the cloud and used by WinterGram to enable advanced features.";
"PrivateDataSettings.Title" = "Data Settings";
"Privacy.ChatsTitle" = "CHATS";
@@ -3781,8 +3781,8 @@ Unused sets are archived when you add more.";
"SocksProxySetup.PasteFromClipboard" = "Paste From Clipboard";
-"Share.AuthTitle" = "Log in to Telegram";
-"Share.AuthDescription" = "Open Telegram and log in to share.";
+"Share.AuthTitle" = "Log in to WinterGram";
+"Share.AuthDescription" = "Open WinterGram and log in to share.";
"Notifications.DisplayNamesOnLockScreen" = "Names on lock-screen";
"Notifications.DisplayNamesOnLockScreenInfoWithLink" = "Display names in notifications when the device is locked. To disable, make sure that \"Show Previews\" is also set to \"When Unlocked\" or \"Never\" in [iOS Settings]";
@@ -3828,7 +3828,7 @@ Unused sets are archived when you add more.";
"ApplyLanguage.ChangeLanguageAction" = "Change";
"ApplyLanguage.ApplyLanguageAction" = "Change";
"ApplyLanguage.UnsufficientDataTitle" = "Insufficient Data";
-"ApplyLanguage.UnsufficientDataText" = "Unfortunately, this custom language pack (%1$@) doesn't contain data for Telegram iOS. You can contribute to this language pack using the [translations platform]()";
+"ApplyLanguage.UnsufficientDataText" = "Unfortunately, this custom language pack (%1$@) doesn't contain data for WinterGram iOS. You can contribute to this language pack using the [translations platform]()";
"ApplyLanguage.LanguageNotSupportedError" = "Sorry, this language doesn't seem to exist.";
"ApplyLanguage.ApplySuccess" = "Language changed";
@@ -3869,8 +3869,8 @@ Unused sets are archived when you add more.";
"InstantPage.TapToOpenLink" = "Tap to open the link:";
"InstantPage.RelatedArticleAuthorAndDateTitle" = "%1$@ • %2$@";
-"AuthCode.Alert" = "Your login code is %@. Enter it in the Telegram app where you are trying to log in.\n\nDo not give this code to anyone.";
-"Login.CheckOtherSessionMessages" = "Check your Telegram messages";
+"AuthCode.Alert" = "Your login code is %@. Enter it in the WinterGram app where you are trying to log in.\n\nDo not give this code to anyone.";
+"Login.CheckOtherSessionMessages" = "Check your WinterGram messages";
"Login.SendCodeViaSms" = "Get the code via SMS";
"Login.SendCodeViaCall" = "Call me to dictate the code";
"Login.SendCodeViaFlashCall" = "Get the code via phone call";
@@ -3880,7 +3880,7 @@ Unused sets are archived when you add more.";
"Login.CodeExpired" = "Code expired, please login again.";
"Login.CancelSignUpConfirmation" = "Do you want to stop the registration process?";
-"Passcode.AppLockedAlert" = "Telegram\nLocked";
+"Passcode.AppLockedAlert" = "WinterGram\nLocked";
"ChatList.ReadAll" = "Read All";
"ChatList.Read" = "Read";
@@ -3900,7 +3900,7 @@ Unused sets are archived when you add more.";
"Permissions.Skip" = "Skip";
"Permissions.ContactsTitle.v0" = "Sync Your Contacts";
-"Permissions.ContactsText.v0" = "See who's on Telegram and switch seamlessly, without having to \"add\" your friends.";
+"Permissions.ContactsText.v0" = "See who's on WinterGram and switch seamlessly, without having to \"add\" your friends.";
"Permissions.ContactsAllow.v0" = "Allow Access";
"Permissions.ContactsAllowInSettings.v0" = "Allow in Settings";
@@ -3911,7 +3911,7 @@ Unused sets are archived when you add more.";
"Permissions.NotificationsAllowInSettings.v0" = "Turn ON in Settings";
"Permissions.CellularDataTitle.v0" = "Enable Cellular Data";
-"Permissions.CellularDataText.v0" = "Don't worry, Telegram keeps network usage to a minimum. You can further control this in Settings > Data and Storage.";
+"Permissions.CellularDataText.v0" = "Don't worry, WinterGram keeps network usage to a minimum. You can further control this in Settings > Data and Storage.";
"Permissions.CellularDataAllowInSettings.v0" = "Turn ON in Settings";
"Permissions.SiriTitle.v0" = "Turn ON Siri";
@@ -3922,11 +3922,11 @@ Unused sets are archived when you add more.";
"Permissions.PrivacyPolicy" = "Privacy Policy";
"Contacts.PermissionsTitle" = "Access to Contacts";
-"Contacts.PermissionsText" = "Please allow Telegram access to your phonebook to seamlessly find all your friends.";
+"Contacts.PermissionsText" = "Please allow WinterGram access to your phonebook to seamlessly find all your friends.";
"Contacts.PermissionsAllow" = "Allow Access";
"Contacts.PermissionsAllowInSettings" = "Allow in Settings";
"Contacts.PermissionsSuppressWarningTitle" = "Keep contacts disabled?";
-"Contacts.PermissionsSuppressWarningText" = "You won't know when your friends join Telegram and become available to chat. We recommend enabling access to contacts in Settings.";
+"Contacts.PermissionsSuppressWarningText" = "You won't know when your friends join WinterGram and become available to chat. We recommend enabling access to contacts in Settings.";
"Contacts.PermissionsKeepDisabled" = "Keep Disabled";
"Contacts.PermissionsEnable" = "Enable";
@@ -3938,7 +3938,7 @@ Unused sets are archived when you add more.";
"Notifications.PermissionsAllowInSettings" = "Turn ON in Settings";
"Notifications.PermissionsOpenSettings" = "Open Settings";
"Notifications.PermissionsSuppressWarningTitle" = "Keep notifications disabled?";
-"Notifications.PermissionsSuppressWarningText" = "You may miss important messages on Telegram due to your current settings.\n\nFor better results, enable alerts or banners and try muting certain chats or chat types in Telegram settings.";
+"Notifications.PermissionsSuppressWarningText" = "You may miss important messages on WinterGram due to your current settings.\n\nFor better results, enable alerts or banners and try muting certain chats or chat types in WinterGram settings.";
"Notifications.PermissionsKeepDisabled" = "Keep Disabled";
"Notifications.PermissionsEnable" = "Enable";
@@ -3995,7 +3995,7 @@ Unused sets are archived when you add more.";
"AttachmentMenu.WebSearch" = "Web Search";
-"Conversation.UnsupportedMediaPlaceholder" = "This message is not supported on your version of Telegram. Please update to the latest version.";
+"Conversation.UnsupportedMediaPlaceholder" = "This message is not supported on your version of WinterGram. Please update to the latest version.";
"Conversation.UpdateTelegram" = "UPDATE TELEGRAM";
"Cache.LowDiskSpaceText" = "Your phone has run out of available storage. Please free some space to download or upload media.";
@@ -4006,7 +4006,7 @@ Unused sets are archived when you add more.";
"Contacts.SortedByName" = "Sorted by Name";
"Contacts.SortedByPresence" = "Sorted by Last Seen Time";
-"NotificationSettings.ContactJoinedInfo" = "Receive push notifications when one of your contacts becomes available on Telegram.";
+"NotificationSettings.ContactJoinedInfo" = "Receive push notifications when one of your contacts becomes available on WinterGram.";
"GroupInfo.Permissions" = "Permissions";
"GroupInfo.Permissions.Title" = "Permissions";
@@ -4133,7 +4133,7 @@ Unused sets are archived when you add more.";
"Undo.DeletedChannel" = "Deleted channel";
"Undo.DeletedGroup" = "Deleted group";
-"AccessDenied.Wallpapers" = "Telegram needs access to your photo library to set a custom chat background.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON.";
+"AccessDenied.Wallpapers" = "WinterGram needs access to your photo library to set a custom chat background.\n\nPlease go to Settings > Privacy > Photos and set WinterGram to ON.";
"Conversation.ChatBackground" = "Chat Background";
"Conversation.ViewBackground" = "VIEW BACKGROUND";
@@ -4432,14 +4432,14 @@ Unused sets are archived when you add more.";
"ChatList.ClearChatConfirmation" = "Are you sure you want to delete all\nmessages in the chat with %@?";
"Settings.CheckPhoneNumberTitle" = "Is %@ still your number?";
-"Settings.CheckPhoneNumberText" = "Keep your number up to date to ensure you can always log in to Telegram. [Learn more]()";
+"Settings.CheckPhoneNumberText" = "Keep your number up to date to ensure you can always log in to WinterGram. [Learn more]()";
"Settings.KeepPhoneNumber" = "Keep %@";
"Settings.ChangePhoneNumber" = "Change Number";
"Settings.CheckPhoneNumberFAQAnchor" = "q-i-have-a-new-phone-number-what-do-i-do";
"Undo.ChatDeletedForBothSides" = "Chat deleted for both sides";
-"AppUpgrade.Running" = "Optimizing Telegram...
+"AppUpgrade.Running" = "Optimizing WinterGram...
This may take a while, depending on the size of the database. Please keep the app open until the process is finished.
Sorry for the inconvenience.";
@@ -4503,8 +4503,8 @@ Sorry for the inconvenience.";
"Privacy.AddNewPeer" = "Add Users or Groups";
"PrivacyPhoneNumberSettings.WhoCanSeeMyPhoneNumber" = "WHO CAN SEE MY PHONE NUMBER";
-"PrivacyPhoneNumberSettings.CustomHelp" = "Users who already have your number saved in the contacts will also see it on Telegram.";
-"PrivacyPhoneNumberSettings.CustomDisabledHelp" = "Users who add your number to their contacts will see it on Telegram only if they are your contacts.";
+"PrivacyPhoneNumberSettings.CustomHelp" = "Users who already have your number saved in the contacts will also see it on WinterGram.";
+"PrivacyPhoneNumberSettings.CustomDisabledHelp" = "Users who add your number to their contacts will see it on WinterGram only if they are your contacts.";
"PrivacyPhoneNumberSettings.DiscoveryHeader" = "WHO CAN FIND ME BY MY NUMBER";
@@ -4515,7 +4515,7 @@ Sorry for the inconvenience.";
"PrivacySettings.PasscodeOff" = "Off";
"PrivacySettings.PasscodeOn" = "On";
-"UserInfo.BlockConfirmationTitle" = "Do you want to block %@ from messaging and calling you on Telegram?";
+"UserInfo.BlockConfirmationTitle" = "Do you want to block %@ from messaging and calling you on WinterGram?";
"UserInfo.BlockActionTitle" = "Block %@";
"ReportSpam.DeleteThisChat" = "Delete this Chat";
@@ -4680,7 +4680,7 @@ Sorry for the inconvenience.";
"Group.PublicLink.Title" = "Public Link";
"Group.PublicLink.Placeholder" = "link";
-"Group.PublicLink.Info" = "People can share this link with others and find your group using Telegram search.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
+"Group.PublicLink.Info" = "People can share this link with others and find your group using WinterGram search.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
"CreateGroup.ErrorLocatedGroupsTooMuch" = "Sorry, you have too many location-based groups already. Please delete one of your existing ones first.";
@@ -5020,12 +5020,12 @@ Sorry for the inconvenience.";
"TwoFactorSetup.Password.Action" = "Create Password";
"TwoFactorSetup.Email.Title" = "Recovery Email";
-"TwoFactorSetup.Email.Text" = "You can set a recovery email to be able to reset you password and restore access to your Telegram account.";
+"TwoFactorSetup.Email.Text" = "You can set a recovery email to be able to reset you password and restore access to your WinterGram account.";
"TwoFactorSetup.Email.Placeholder" = "Your email address";
"TwoFactorSetup.Email.Action" = "Continue";
"TwoFactorSetup.Email.SkipAction" = "Skip setting email";
"TwoFactorSetup.Email.SkipConfirmationTitle" = "No, seriously.";
-"TwoFactorSetup.Email.SkipConfirmationText" = "If you forget your password, you will lose access to your Telegram account. There will be no way to restore it.";
+"TwoFactorSetup.Email.SkipConfirmationText" = "If you forget your password, you will lose access to your WinterGram account. There will be no way to restore it.";
"TwoFactorSetup.Email.SkipConfirmationSkip" = "Skip";
"TwoFactorSetup.EmailVerification.Title" = "Recovery Email";
@@ -5059,11 +5059,11 @@ Sorry for the inconvenience.";
"Group.ErrorSupergroupConversionNotPossible" = "Sorry, you are a member of too many groups and channels. Please leave some before creating a new one.";
"ClearCache.StorageTitle" = "%@ STORAGE";
-"ClearCache.StorageCache" = "Telegram Cache";
-"ClearCache.StorageServiceFiles" = "Telegram Service Files";
+"ClearCache.StorageCache" = "WinterGram Cache";
+"ClearCache.StorageServiceFiles" = "WinterGram Service Files";
"ClearCache.StorageOtherApps" = "Other Apps";
"ClearCache.StorageFree" = "Free";
-"ClearCache.ClearCache" = "Clear Telegram Cache";
+"ClearCache.ClearCache" = "Clear WinterGram Cache";
"ClearCache.Clear" = "Clear";
"ClearCache.Forever" = "Forever";
@@ -5127,8 +5127,8 @@ Sorry for the inconvenience.";
"AuthSessions.AddDevice.ScanTitle" = "Scan QR Code";
"AuthSessions.AddDevice.InvalidQRCode" = "Invalid QR Code";
"AuthSessions.AddDeviceIntro.Title" = "Log in by QR Code";
-"AuthSessions.AddDeviceIntro.Text1" = "Download Telegram on your computer from [desktop.telegram.org]()";
-"AuthSessions.AddDeviceIntro.Text2" = "Run Telegram on your computer to get the QR code";
+"AuthSessions.AddDeviceIntro.Text1" = "Download WinterGram on your computer from [desktop.telegram.org]()";
+"AuthSessions.AddDeviceIntro.Text2" = "Run WinterGram on your computer to get the QR code";
"AuthSessions.AddDeviceIntro.Text3" = "Scan the QR code to connect your account";
"AuthSessions.AddDeviceIntro.Action" = "Scan QR Code";
"AuthSessions.AddedDeviceTitle" = "Login Successful";
@@ -5141,13 +5141,13 @@ Sorry for the inconvenience.";
"Map.Home" = "Home";
"Map.Work" = "Work";
"Map.HomeAndWorkTitle" = "Home & Work Addresses";
-"Map.HomeAndWorkInfo" = "Telegram uses the Home and Work addresses from your Contact Card.\n\nKeep your Contact Card up to date for quick access to sending Home and Work addresses.";
+"Map.HomeAndWorkInfo" = "WinterGram uses the Home and Work addresses from your Contact Card.\n\nKeep your Contact Card up to date for quick access to sending Home and Work addresses.";
"Map.SearchNoResultsDescription" = "There were no results for \"%@\".\nTry a new search.";
"ChatList.Search.ShowMore" = "Show more";
"ChatList.Search.ShowLess" = "Show less";
-"AuthSessions.OtherDevices" = "The official Telegram App is available for iPhone, iPad, Android, macOS, Windows and Linux. [Learn More]()";
+"AuthSessions.OtherDevices" = "The official WinterGram App is available for iPhone, iPad, Android, macOS, Windows and Linux. [Learn More]()";
"MediaPlayer.UnknownArtist" = "Unknown Artist";
"MediaPlayer.UnknownTrack" = "Unknown Track";
@@ -5165,7 +5165,7 @@ Sorry for the inconvenience.";
"Theme.Colors.Proceed" = "Proceed";
-"AuthSessions.AddDevice.UrlLoginHint" = "This code can be used to allow someone to log in to your Telegram account.\n\nTo confirm Telegram login, please go to Settings > Devices > Scan QR and scan the code.";
+"AuthSessions.AddDevice.UrlLoginHint" = "This code can be used to allow someone to log in to your WinterGram account.\n\nTo confirm WinterGram login, please go to Settings > Devices > Scan QR and scan the code.";
"Appearance.RemoveThemeColor" = "Remove";
"Appearance.RemoveThemeColorConfirmation" = "Remove Color";
@@ -5663,7 +5663,7 @@ Sorry for the inconvenience.";
"Cache.MaximumCacheSize" = "Maximum Cache Size";
"Cache.NoLimit" = "No Limit";
-"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
+"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the WinterGram cloud and can be re-downloaded if you need it again.";
"Stats.MessageTitle" = "Message Statistics";
"Stats.MessageOverview" = "Overview";
@@ -5684,9 +5684,9 @@ Sorry for the inconvenience.";
"Call.Audio" = "audio";
"Call.AudioRouteMute" = "Mute Yourself";
-"AccessDenied.VideoCallCamera" = "Telegram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
+"AccessDenied.VideoCallCamera" = "WinterGram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set WinterGram to ON.";
-"Call.AccountIsLoggedOnCurrentDevice" = "Sorry, you can't call %@ because that account is logged in to Telegram on the device you're using for the call.";
+"Call.AccountIsLoggedOnCurrentDevice" = "Sorry, you can't call %@ because that account is logged in to WinterGram on the device you're using for the call.";
"ChatList.Search.FilterChats" = "Chats";
"ChatList.Search.FilterMedia" = "Media";
@@ -5838,7 +5838,7 @@ Sorry for the inconvenience.";
"Conversation.EditingPhotoPanelTitle" = "Edit Photo";
"Media.LimitedAccessTitle" = "Limited Access to Media";
-"Media.LimitedAccessText" = "You've given Telegram access only to select number of photos.";
+"Media.LimitedAccessText" = "You've given WinterGram access only to select number of photos.";
"Media.LimitedAccessManage" = "Manage";
"Media.LimitedAccessSelectMore" = "Select More Photos...";
"Media.LimitedAccessChangeSettings" = "Change Settings";
@@ -6038,8 +6038,8 @@ Sorry for the inconvenience.";
"InviteLink.PeopleJoined_3_10" = "%@ people joined";
"InviteLink.PeopleJoined_many" = "%@ people joined";
"InviteLink.PeopleJoined_any" = "%@ people joined";
-"InviteLink.CreatePrivateLinkHelp" = "Anyone who has Telegram installed will be able to join your group by following this link.";
-"InviteLink.CreatePrivateLinkHelpChannel" = "Anyone who has Telegram installed will be able to join your channel by following this link.";
+"InviteLink.CreatePrivateLinkHelp" = "Anyone who has WinterGram installed will be able to join your group by following this link.";
+"InviteLink.CreatePrivateLinkHelpChannel" = "Anyone who has WinterGram installed will be able to join your channel by following this link.";
"InviteLink.Manage" = "Manage Invite Links";
"InviteLink.PeopleJoinedShortNoneExpired" = "no one joined";
@@ -6099,8 +6099,8 @@ Sorry for the inconvenience.";
"InviteLink.Create.Revoke" = "Revoke Link";
"InviteLink.QRCode.Title" = "Invite by QR Code";
-"InviteLink.QRCode.Info" = "Everyone on Telegram can scan this code to join your group.";
-"InviteLink.QRCode.InfoChannel" = "Everyone on Telegram can scan this code to join your channel.";
+"InviteLink.QRCode.Info" = "Everyone on WinterGram can scan this code to join your group.";
+"InviteLink.QRCode.InfoChannel" = "Everyone on WinterGram can scan this code to join your channel.";
"InviteLink.QRCode.Share" = "Share QR Code";
"InviteLink.InviteLink" = "Invite Link";
@@ -6182,7 +6182,7 @@ Sorry for the inconvenience.";
"Message.ImportedDateFormat" = "%1$@, %2$@ Imported %3$@";
"ChatImportActivity.Title" = "Importing Chat";
-"ChatImportActivity.OpenApp" = "Open Telegram";
+"ChatImportActivity.OpenApp" = "Open WinterGram";
"ChatImportActivity.Retry" = "Retry";
"ChatImportActivity.InProgress" = "Please keep this window open\nuntil the import is completed.";
"ChatImportActivity.ErrorNotAdmin" = "You need to be an admin in the group to import messages.";
@@ -6244,7 +6244,7 @@ Sorry for the inconvenience.";
"Report.AdditionalDetailsText" = "Please enter any additional details relevant for your report.";
"Report.AdditionalDetailsPlaceholder" = "Additional details...";
"Report.Report" = "Report";
-"Report.Succeed" = "Telegram moderators will study your report. Thank you!";
+"Report.Succeed" = "WinterGram moderators will study your report. Thank you!";
"Conversation.AutoremoveRemainingTime" = "auto-deletes in %@";
"Conversation.AutoremoveRemainingDays_1" = "auto-deletes in %@ day";
@@ -6334,7 +6334,7 @@ Sorry for the inconvenience.";
"Widget.UpdatedAt" = "Updated {}";
"Intents.ErrorLockedTitle" = "Locked";
-"Intents.ErrorLockedText" = "Open Telegram and enter passcode to edit widget.";
+"Intents.ErrorLockedText" = "Open WinterGram and enter passcode to edit widget.";
"Conversation.GigagroupDescription" = "Only admins can send messages in this group.";
@@ -6355,7 +6355,7 @@ Sorry for the inconvenience.";
"Conversation.UploadFileTooLarge" = "File could not be sent, because it is larger than 2 GB.\n\nYou can send as many files as you like, but each must be smaller than 2 GB.";
-"Channel.AddUserLeftError" = "Sorry, if a person is no longer part of a channel, you need to be in their Telegram contacts in order to add them back.\n\nNote that they can still join via the channel's invite link as long as they are not in the Removed Users list.";
+"Channel.AddUserLeftError" = "Sorry, if a person is no longer part of a channel, you need to be in their WinterGram contacts in order to add them back.\n\nNote that they can still join via the channel's invite link as long as they are not in the Removed Users list.";
"Message.ScamAccount" = "Scam";
"Message.FakeAccount" = "Fake";
@@ -6640,7 +6640,7 @@ Sorry for the inconvenience.";
"ScheduledIn.Years_any" = "%@ years";
"ScheduledIn.Months_many" = "%@ years";
-"Checkout.PaymentLiabilityAlert" = "Neither Telegram, nor {target} will have access to your credit card information. Credit card details will be handled only by the payment system, {payment_system}.\n\nPayments will go directly to the developer of {target}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank.";
+"Checkout.PaymentLiabilityAlert" = "Neither WinterGram, nor {target} will have access to your credit card information. Credit card details will be handled only by the payment system, {payment_system}.\n\nPayments will go directly to the developer of {target}. WinterGram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank.";
"Checkout.OptionalTipItem" = "Tip (Optional)";
"Checkout.TipItem" = "Tip";
@@ -6658,8 +6658,8 @@ Sorry for the inconvenience.";
"Privacy.PaymentsClear.ShippingInfoCleared" = "Shipping info cleared.";
"Privacy.PaymentsClear.AllInfoCleared" = "Payment and shipping info cleared.";
-"Settings.Tips" = "Telegram Features";
-"Settings.TipsUsername" = "TelegramTips";
+"Settings.Tips" = "WinterGram Features";
+"Settings.TipsUsername" = "WinterGramTips";
"Calls.NoVoiceAndVideoCallsPlaceholder" = "Your recent voice and video calls will appear here.";
"Calls.StartNewCall" = "Start New Call";
@@ -6941,7 +6941,7 @@ Sorry for the inconvenience.";
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
"SponsoredMessageInfoScreen.Title" = "What are sponsored messages?";
-"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Telegram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
+"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, WinterGram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on WinterGram sees the same sponsored message.\n\nUnline other apps, WinterGram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nWinterGram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, WinterGram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
"SponsoredMessageInfo.Action" = "Learn More";
"SponsoredMessageInfo.Url" = "https://telegram.org/ads";
@@ -7237,7 +7237,7 @@ Sorry for the inconvenience.";
"Conversation.InviteRequestInfo" = "You received this message because you requested to join %1$@ on %2$@.";
"Conversation.InviteRequestInfoConfirm" = "I understand";
-"AuthSessions.HeaderInfo" = "Link [Telegram Desktop](desktop) or [Telegram Web](web) by scanning a QR code.";
+"AuthSessions.HeaderInfo" = "Link [WinterGram Desktop](desktop) or [WinterGram Web](web) by scanning a QR code.";
"AuthSessions.LinkDesktopDevice" = "Link Desktop Device";
"AuthSessions.AddDevice.ScanInstallInfo" = "Go to [getdesktop.telegram.org](desktop) or [web.telegram.org](web) to get the QR code";
@@ -7288,7 +7288,7 @@ Sorry for the inconvenience.";
"Conversation.ContextMenuTranslate" = "Translate";
-"ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need them again.";
+"ClearCache.ClearDescription" = "All media will stay in the WinterGram cloud and can be re-downloaded if you need them again.";
"ChatSettings.StickersAndReactions" = "Stickers and Emoji";
@@ -7310,8 +7310,8 @@ Sorry for the inconvenience.";
"Contacts.QrCode.MyCode" = "My QR Code";
"Contacts.QrCode.NoCodeFound" = "No valid QR code found in the image. Please try again.";
-"AccessDenied.QrCode" = "Telegram needs access to your photo library to scan QR codes.\n\nOpen your device's Settings > Privacy > Photos and set Telegram to ON.";
-"AccessDenied.QrCamera" = "Telegram needs access to your camera to scan QR codes.\n\nOpen your device's Settings > Privacy > Camera and set Telegram to ON.";
+"AccessDenied.QrCode" = "WinterGram needs access to your photo library to scan QR codes.\n\nOpen your device's Settings > Privacy > Photos and set WinterGram to ON.";
+"AccessDenied.QrCamera" = "WinterGram needs access to your camera to scan QR codes.\n\nOpen your device's Settings > Privacy > Camera and set WinterGram to ON.";
"Share.ShareToInstagramStories" = "Share to Instagram Stories";
@@ -7424,8 +7424,8 @@ Sorry for the inconvenience.";
"Attachment.MediaAccessText" = "Share an unlimited number of photos and videos of up to 2 GB each.";
"Attachment.MediaAccessStoryText" = "Share an unlimited number of photos and videos of up to 2 GB each.";
-"Attachment.LimitedMediaAccessText" = "You have limited Telegram from accessing all of your photos.";
-"Attachment.CameraAccessText" = "Telegram needs camera access so that you can take photos and videos.";
+"Attachment.LimitedMediaAccessText" = "You have limited WinterGram from accessing all of your photos.";
+"Attachment.CameraAccessText" = "WinterGram needs camera access so that you can take photos and videos.";
"Attachment.Manage" = "Manage";
"Attachment.OpenSettings" = "Go to Settings";
@@ -7440,7 +7440,7 @@ Sorry for the inconvenience.";
"Attachment.DeselectedItems_1" = "%@ item deselected";
"Attachment.DeselectedItems_any" = "%@ items deselected";
-"PrivacyPhoneNumberSettings.CustomPublicLink" = "Users who have your number saved in their contacts will also see it on Telegram.\n\nThis public link opens a chat with you:\n[https://t.me/%@]()";
+"PrivacyPhoneNumberSettings.CustomPublicLink" = "Users who have your number saved in their contacts will also see it on WinterGram.\n\nThis public link opens a chat with you:\n[https://t.me/%@]()";
"DownloadList.DownloadingHeader" = "Downloading";
"DownloadList.DownloadedHeader" = "Recently Downloaded";
@@ -7460,7 +7460,7 @@ Sorry for the inconvenience.";
"DownloadList.RemoveFileAlertRemove" = "Remove";
"DownloadList.ClearAlertTitle" = "Downloaded Files";
-"DownloadList.ClearAlertText" = "Telegram allows to store all received and sent\ndocuments in the cloud and save storage\nspace on your device.";
+"DownloadList.ClearAlertText" = "WinterGram allows to store all received and sent\ndocuments in the cloud and save storage\nspace on your device.";
"ChatList.Search.FilterDownloads" = "Downloads";
@@ -7469,8 +7469,8 @@ Sorry for the inconvenience.";
"LiveStream.ViewerCount_any" = "%@ viewers";
"LiveStream.Watching" = "watching";
-"LiveStream.NoSignalAdminText" = "Oops! Telegram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app.";
-"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Telegram.";
+"LiveStream.NoSignalAdminText" = "Oops! WinterGram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app.";
+"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to WinterGram.";
"LiveStream.ViewCredentials" = "View Stream Key";
@@ -7559,7 +7559,7 @@ Sorry for the inconvenience.";
"Notifications.UploadError.TooLong.Title" = "%@ is too long.";
"Notifications.UploadError.TooLong.Text" = "Duration must be less than %@.";
"Notifications.UploadSuccess.Title" = "Sound Added";
-"Notifications.UploadSuccess.Text" = "The sound **%@** was added to your Telegram tones.";
+"Notifications.UploadSuccess.Text" = "The sound **%@** was added to your WinterGram tones.";
"Notifications.SaveSuccess.Text" = "You can now use this sound as a notification tone in your [custom notification settings]().";
"Conversation.DeleteTimer.SetupTitle" = "Auto-Delete After...";
@@ -7649,31 +7649,31 @@ Sorry for the inconvenience.";
"Channel.AddUserKickedError" = "Sorry, you can't add this user because they are on the list of Removed Users and you can't unban them.";
"Channel.AddAdminKickedError" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them.";
-"Premium.Stickers.Description" = "Unlock this sticker and many more by subscribing to Telegram Premium.";
+"Premium.Stickers.Description" = "Unlock this sticker and many more by subscribing to WinterGram Premium.";
"Premium.Stickers.Proceed" = "Unlock Premium Stickers";
"Premium.Reactions.Proceed" = "Unlock Premium Reactions";
"Premium.AppIcons.Proceed" = "Unlock Premium Icons";
-"Premium.NoAds.Proceed" = "About Telegram Premium";
+"Premium.NoAds.Proceed" = "About WinterGram Premium";
-"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
+"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > WinterGram and set Precise Location to On.";
"Chat.MultipleTypingPair" = "%@ and %@";
"Chat.MultipleTypingMore" = "%@ and %@ others";
-"Group.Username.RemoveExistingUsernamesOrExtendInfo" = "You have reserved too many public links. Try revoking a link from an older group or channel, or upgrade to **Telegram Premium** to double the limit to **%@** public links.";
+"Group.Username.RemoveExistingUsernamesOrExtendInfo" = "You have reserved too many public links. Try revoking a link from an older group or channel, or upgrade to **WinterGram Premium** to double the limit to **%@** public links.";
"Group.Username.RemoveExistingUsernamesNoPremiumInfo" = "You have reserved too many public links. Try revoking the link from an older group or channel. We are working to let you increase this limit in the future.";
"Group.Username.RemoveExistingUsernamesFinalInfo" = "You have reserved too many public links. Try revoking the link from an older group or channel, or create a private one instead.";
-"OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
+"OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **WinterGram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesNoPremiumText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesFinalText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one.";
-"OldChannels.TooManyCommunitiesCreateText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
+"OldChannels.TooManyCommunitiesCreateText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one or upgrade to **WinterGram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesCreateNoPremiumText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesCreateFinalText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one.";
-"OldChannels.TooManyCommunitiesUpgradeText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
+"OldChannels.TooManyCommunitiesUpgradeText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups or upgrade to **WinterGram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesUpgradeNoPremiumText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesUpgradeFinalText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups.";
@@ -7684,19 +7684,19 @@ Sorry for the inconvenience.";
"Premium.LimitReached" = "Limit Reached";
"Premium.IncreaseLimit" = "Increase Limit";
-"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **Telegram Premium**.";
+"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **WinterGram Premium**.";
"Premium.MaxFoldersCountNoPremiumText" = "You have reached the limit of **%1$@** folders. We are working to let you increase this limit in the future.";
"Premium.MaxFoldersCountFinalText" = "Sorry, you can't create more than **%1$@** folders.";
-"Premium.MaxChatsInFolderText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **Telegram Premium**.";
+"Premium.MaxChatsInFolderText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **WinterGram Premium**.";
"Premium.MaxChatsInFolderNoPremiumText" = "Sorry, you can't add more than **%1$@** chats to a folder. We are working to let you increase this limit in the future.";
"Premium.MaxChatsInFolderFinalText" = "Sorry, you can't add more than **%@** chats to a folder.";
-"Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **Telegram Premium**.";
+"Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **WinterGram Premium**.";
"Premium.MaxFileSizeNoPremiumText" = "The document can't be sent, because it is larger than **%@**. We are working to let you increase this limit in the future.";
"Premium.MaxFileSizeFinalText" = "The document can't be sent, because it is larger than **%@**.";
-"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
+"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **WinterGram Premium** to double the limit to **%2$@** chats.";
"Premium.MaxPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned.";
"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned.";
@@ -7708,21 +7708,21 @@ Sorry for the inconvenience.";
"Premium.MaxSavedGifsText" = "An older GIF was replaced with this one. You can [increase the limit]() to %@ GIFS.";
"Premium.MaxSavedGifsFinalText" = "An older GIF was replaced with this one.";
-"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can add more by subscribing to **Telegram Premium**.";
+"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can add more by subscribing to **WinterGram Premium**.";
"Premium.MaxAccountsNoPremiumText" = "You have reached the limit of **%@** connected accounts.";
"Premium.MaxAccountsFinalText" = "You have reached the limit of **%@** connected accounts.";
"Premium.Free" = "Free";
"Premium.Premium" = "Premium";
-"Premium.Title" = "Telegram Premium";
-"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**.";
+"Premium.Title" = "WinterGram Premium";
+"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **WinterGram Premium**.";
-"Premium.PersonalTitle" = "[%@]() is a subscriber\nof Telegram Premium";
-"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features.";
+"Premium.PersonalTitle" = "[%@]() is a subscriber\nof WinterGram Premium";
+"Premium.PersonalDescription" = "Owners of **WinterGram Premium** accounts have exclusive access to multiple additional features.";
"Premium.SubscribedTitle" = "You are all set!";
-"Premium.SubscribedDescription" = "Thank you for subsribing to **Telegram Premium**. Here's what is now unlocked.";
+"Premium.SubscribedDescription" = "Thank you for subsribing to **WinterGram Premium**. Here's what is now unlocked.";
"Premium.DoubledLimits" = "Doubled Limits";
"Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more.";
@@ -7732,48 +7732,48 @@ Sorry for the inconvenience.";
"Premium.FasterSpeed" = "Faster Download Speed";
"Premium.FasterSpeedInfo" = "No more limits on the speed with which media and documents are downloaded.";
-"Premium.FasterSpeedStandaloneInfo" = "Subscribe to **Telegram Premium** to download media and files at the fastest possible speed.";
+"Premium.FasterSpeedStandaloneInfo" = "Subscribe to **WinterGram Premium** to download media and files at the fastest possible speed.";
"Premium.VoiceToText" = "Voice-to-Text Conversion";
"Premium.VoiceToTextInfo" = "Ability to read the transcript of any incoming voice message.";
-"Premium.VoiceToTextStandaloneInfo" = "Subscribe to **Telegram Premium** to be able to convert voice and video messages to text.";
+"Premium.VoiceToTextStandaloneInfo" = "Subscribe to **WinterGram Premium** to be able to convert voice and video messages to text.";
"Premium.NoAds" = "No Ads";
-"Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads.";
-"Premium.NoAdsStandaloneInfo" = "Remove ads such as this one by subscribing to **Telegram Premium**.";
+"Premium.NoAdsInfo" = "No more ads in public channels where WinterGram sometimes shows ads.";
+"Premium.NoAdsStandaloneInfo" = "Remove ads such as this one by subscribing to **WinterGram Premium**.";
"Premium.Reactions" = "Unique Reactions";
"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to Premium subscribers.";
"Premium.ReactionsStandalone" = "Additional Reactions";
-"Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions on messages by subscribing to **Telegram Premium**.";
+"Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions on messages by subscribing to **WinterGram Premium**.";
"Premium.Stickers" = "Premium Stickers";
"Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
"Premium.ChatManagement" = "Advanced Chat Management";
"Premium.ChatManagementInfo" = "Tools to set the default folder, auto-archive and hide new chats from non-contacts.";
-"Premium.ChatManagementStandaloneInfo" = "Subscribers of **Telegram Premium** can set the default folder, auto-archive and hide new chats from non-contacts.";
+"Premium.ChatManagementStandaloneInfo" = "Subscribers of **WinterGram Premium** can set the default folder, auto-archive and hide new chats from non-contacts.";
"Premium.Badge" = "Profile Badge";
-"Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram.";
+"Premium.BadgeInfo" = "A badge next to your name showing that you are helping support WinterGram.";
"Premium.Avatar" = "Animated Profile Pictures";
"Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
-"Premium.AppIcon" = "Telegram App Icon";
-"Premium.AppIconInfo" = "Choose from a selection of Telegram app icons for your homescreen.";
+"Premium.AppIcon" = "WinterGram App Icon";
+"Premium.AppIconInfo" = "Choose from a selection of WinterGram app icons for your homescreen.";
"Premium.AppIconStandalone" = "Additional App Icons";
-"Premium.AppIconStandaloneInfo" = "Unlock a wider range of app icons by subscribing to **Telegram Premium**.";
+"Premium.AppIconStandaloneInfo" = "Unlock a wider range of app icons by subscribing to **WinterGram Premium**.";
"Premium.SubscribeFor" = "Subscribe for %@ / month";
"Premium.SubscribeForAnnual" = "Subscribe for %@ / year";
"Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM";
-"Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
+"Premium.AboutText" = "While the free version of WinterGram already gives its users more than any other messaging application, **WinterGram Premium** pushes its capabilities even further.\n\n**WinterGram Premium** is a paid option, because most Premium Features require additional expenses from WinterGram to third parties such as data center providers and server manufacturers. Contributions from **WinterGram Premium** users allow us to cover such costs and also help WinterGram stay free for everyone.";
-"Premium.Terms" = "By purchasing a Premium subscription, you agree to the Telegram [Terms of Service](terms) and [Privacy Policy](privacy).";
+"Premium.Terms" = "By purchasing a Premium subscription, you agree to the WinterGram [Terms of Service](terms) and [Privacy Policy](privacy).";
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwarding is restricted";
@@ -7834,7 +7834,7 @@ Sorry for the inconvenience.";
"Premium.Restore.Success" = "Done";
"Premium.Restore.ErrorUnknown" = "An error occurred. Please try again.";
-"Settings.Premium" = "Telegram Premium";
+"Settings.Premium" = "WinterGram Premium";
"Settings.AddAnotherAccount.PremiumHelp" = "You can add up to four accounts with different phone numbers.";
@@ -7910,9 +7910,9 @@ Sorry for the inconvenience.";
"PeerInfo.GiftPremium" = "Gift Premium";
-"Premium.Gift.Title" = "Gift Telegram Premium";
-"Premium.Gift.Description" = "Let **%@** enjoy exclusive features of Telegram with **Telegram Premium**.";
-"Premium.Gift.Info" = "You can review the list of features and terms of use for Telegram Premium [here]().";
+"Premium.Gift.Title" = "Gift WinterGram Premium";
+"Premium.Gift.Description" = "Let **%@** enjoy exclusive features of WinterGram with **WinterGram Premium**.";
+"Premium.Gift.Info" = "You can review the list of features and terms of use for WinterGram Premium [here]().";
"Premium.Gift.GiftSubscription" = "Gift Subscription for %@";
"Premium.Gift.Months_1" = "%@ Month";
@@ -7921,16 +7921,16 @@ Sorry for the inconvenience.";
"Premium.Gift.Years_1" = "%@ Year";
"Premium.Gift.Years_any" = "%@ Years";
-"Premium.GiftedTitle" = "Telegram Premium";
+"Premium.GiftedTitle" = "WinterGram Premium";
-"Premium.GiftedTitle.3Month" = "[%@]() has gifted you a 3-month subscription for Telegram Premium";
-"Premium.GiftedTitle.6Month" = "[%@]() has gifted you a 6-month subscription for Telegram Premium";
-"Premium.GiftedTitle.12Month" = "[%@]() has gifted you a 12-month subscription for Telegram Premium";
+"Premium.GiftedTitle.3Month" = "[%@]() has gifted you a 3-month subscription for WinterGram Premium";
+"Premium.GiftedTitle.6Month" = "[%@]() has gifted you a 6-month subscription for WinterGram Premium";
+"Premium.GiftedTitle.12Month" = "[%@]() has gifted you a 12-month subscription for WinterGram Premium";
"Premium.GiftedDescription" = "You now have access to additional features.";
-"Premium.GiftedTitleYou.3Month" = "You gifted [%@]() a 3-month subscription for Telegram Premium";
-"Premium.GiftedTitleYou.6Month" = "You gifted [%@]() a 6-month subscription for Telegram Premium";
-"Premium.GiftedTitleYou.12Month" = "You gifted [%@]() a 12-month subscription for Telegram Premium";
+"Premium.GiftedTitleYou.3Month" = "You gifted [%@]() a 3-month subscription for WinterGram Premium";
+"Premium.GiftedTitleYou.6Month" = "You gifted [%@]() a 6-month subscription for WinterGram Premium";
+"Premium.GiftedTitleYou.12Month" = "You gifted [%@]() a 12-month subscription for WinterGram Premium";
"Premium.GiftedDescriptionYou" = "They now have access to additional features.";
"SettingsSearch.DeleteAccount.DeleteMyAccount" = " ";
@@ -7941,7 +7941,7 @@ Sorry for the inconvenience.";
"Notification.PremiumGift.Months_1" = "%@ month";
"Notification.PremiumGift.Months_any" = "%@ months";
-"Notification.PremiumGift.Title" = "Telegram Premium";
+"Notification.PremiumGift.Title" = "WinterGram Premium";
"Notification.PremiumGift.Subtitle" = "for %@";
"Notification.PremiumGift.View" = "View";
"Notification.PremiumGift.UseGift" = "Use Gift";
@@ -7966,7 +7966,7 @@ Sorry for the inconvenience.";
"Privacy.VoiceMessages" = "Voice Messages";
-"Privacy.VoiceMessages.Tooltip" = "Only subscribers of [Telegram Premium]() can restrict receiving voice messages.";
+"Privacy.VoiceMessages.Tooltip" = "Only subscribers of [WinterGram Premium]() can restrict receiving voice messages.";
"Privacy.VoiceMessages.WhoCanSend" = "WHO CAN SEND ME VOICE MESSAGES";
"Privacy.VoiceMessages.CustomHelp" = "You can restrict who can send you voice messages with granular precision.";
@@ -8048,7 +8048,7 @@ Sorry for the inconvenience.";
"EmojiInput.SectionTitleFavoriteStickers" = "Favorite Stickers";
"EmojiInput.SectionTitlePremiumStickers" = "Premium Stickers";
-"EmojiInput.PremiumEmojiToast.Text" = "Subscribe to Telegram Premium to unlock premium emoji.";
+"EmojiInput.PremiumEmojiToast.Text" = "Subscribe to WinterGram Premium to unlock premium emoji.";
"EmojiInput.PremiumEmojiToast.Action" = "More";
"EmojiInput.PremiumEmojiToast.TryText" = "Try sending these emojis in **Saved Messages** for free to test.";
@@ -8079,13 +8079,13 @@ Sorry for the inconvenience.";
"Login.Edit" = "Edit";
"Login.Yes" = "Yes";
-"Checkout.PaymentLiabilityBothAlert" = "Telegram will not have access to your credit card information. Credit card details will be handled only by the payment system, {target}.\n\nPayments will go directly to the developer of {target}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank.";
+"Checkout.PaymentLiabilityBothAlert" = "WinterGram will not have access to your credit card information. Credit card details will be handled only by the payment system, {target}.\n\nPayments will go directly to the developer of {target}. WinterGram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank.";
"Settings.ChangeProfilePhoto" = "Change Profile Photo";
"Premium.EmojiStatusShortTitle" = "This is %1$@'s current status.";
"Premium.EmojiStatusTitle" = "This is %1$@'s current status from #[%2$@]().";
-"Premium.EmojiStatusText" = "Emoji status is a premium feature.\n Other features included in **Telegram Premium**:";
+"Premium.EmojiStatusText" = "Emoji status is a premium feature.\n Other features included in **WinterGram Premium**:";
"Login.SelectCountry" = "Country";
@@ -8098,7 +8098,7 @@ Sorry for the inconvenience.";
"Login.EnterCodeSMSText" = "We've sent an SMS with an activation code to your phone **%@**.";
"Login.SendCodeAsSMS" = "Send the code as an SMS";
"Login.EnterCodeTelegramTitle" = "Enter Code";
-"Login.EnterCodeTelegramText" = "We've sent the code to the **Telegram app** for %@ on your other device.";
+"Login.EnterCodeTelegramText" = "We've sent the code to the **WinterGram app** for %@ on your other device.";
"Login.AddEmailTitle" = "Add Email";
"Login.AddEmailText" = "Please enter your valid email address to protect your account.";
"Login.AddEmailPlaceholder" = "Enter your email";
@@ -8110,9 +8110,9 @@ Sorry for the inconvenience.";
"Login.WrongCodeError" = "Wrong code, please try again.";
"PrivacySettings.LoginEmail" = "Login Email";
-"PrivacySettings.LoginEmailInfo" = "Change your email address for Telegram login codes.";
+"PrivacySettings.LoginEmailInfo" = "Change your email address for WinterGram login codes.";
"Login.EmailChanged" = "Your email has been changed.";
-"PrivacySettings.LoginEmailAlertText" = "This email address will be used every time you login to your Telegram account from a new device.";
+"PrivacySettings.LoginEmailAlertText" = "This email address will be used every time you login to your WinterGram account from a new device.";
"PrivacySettings.LoginEmailAlertChange" = "Change Email";
"Login.InvalidEmailAddressError" = "An error occurred. Please try again.";
@@ -8127,7 +8127,7 @@ Sorry for the inconvenience.";
"Premium.PricePerMonth" = "%@/month";
"Premium.PricePerYear" = "%@/year";
-"Conversation.SendMesageAsPremiumInfo" = "Subscribe to **Telegram Premium** to be able to comment on behalf your channels in group chats.";
+"Conversation.SendMesageAsPremiumInfo" = "Subscribe to **WinterGram Premium** to be able to comment on behalf your channels in group chats.";
"SetTimeoutFor.Minutes_1" = "Set for 1 minute";
"SetTimeoutFor.Minutes_any" = "Set for %@ minutes";
@@ -8145,7 +8145,7 @@ Sorry for the inconvenience.";
"PeerStatusExpiration.TomorrowAt" = "Your status expires tomorrow at %@";
"PeerStatusExpiration.AtDate" = "Your status expires on %@";
-"Chat.PanelCustomStatusInfo" = "This account uses %@ as a custom status next to its name. Such emoji statuses are available to all subscribers of Telegram Premium.";
+"Chat.PanelCustomStatusInfo" = "This account uses %@ as a custom status next to its name. Such emoji statuses are available to all subscribers of WinterGram Premium.";
"Login.CancelEmailVerification" = "Do you want to stop the email verification process?";
"Login.CancelEmailVerificationStop" = "Stop";
@@ -8158,7 +8158,7 @@ Sorry for the inconvenience.";
"Premium.EmojiStatusInfo" = "Add any of thousands emojis next to your name to display current activity.";
"PeerStatusSetup.NoTimerTitle" = "Long tap to set a timer";
-"Chat.PremiumReactionToastTitle" = "Subscribe to **Telegram Premium** to unlock this reaction.";
+"Chat.PremiumReactionToastTitle" = "Subscribe to **WinterGram Premium** to unlock this reaction.";
"Chat.PremiumReactionToastAction" = "More";
"Chat.ClearReactionsAlertText" = "Do you want to clear your recent reaction emoji from suggestions?";
@@ -8239,7 +8239,7 @@ Sorry for the inconvenience.";
"ChatList.Search.FilterTopics" = "Topics";
"DialogList.SearchSectionTopics" = "Topics";
-"ChatListFolderSettings.SubscribeToMoveAll" = "Subscribe to **Telegram Premium** to move the \"All Chats\" folder.";
+"ChatListFolderSettings.SubscribeToMoveAll" = "Subscribe to **WinterGram Premium** to move the \"All Chats\" folder.";
"ChatListFolderSettings.SubscribeToMoveAllAction" = "More";
"Channel.AdminLog.MessageChangedGroupUsernames" = "%@ changed group links:";
@@ -8257,7 +8257,7 @@ Sorry for the inconvenience.";
"ChatList.EmptyTopicsCreate" = "Create Topic";
"ChatList.EmptyTopicsShowAsMessages" = "Show as Messages";
-"Message.AudioTranscription.SubscribeToPremium" = "Subscribe to **Telegram Premium** to convert voice to text.";
+"Message.AudioTranscription.SubscribeToPremium" = "Subscribe to **WinterGram Premium** to convert voice to text.";
"Message.AudioTranscription.SubscribeToPremiumAction" = "More";
"PeerInfo.PrivateShareLinkInfo" = "This link will work only for group members.";
@@ -8350,8 +8350,8 @@ Sorry for the inconvenience.";
"DownloadList.IncreaseSpeed" = "Increase Speed";
"Conversation.IncreaseSpeed" = "Increase Speed";
-"Premium.ChatManagement.Proceed" = "About Telegram Premium";
-"Premium.FasterSpeed.Proceed" = "About Telegram Premium";
+"Premium.ChatManagement.Proceed" = "About WinterGram Premium";
+"Premium.FasterSpeed.Proceed" = "About WinterGram Premium";
"OwnershipTransfer.EnterPassword" = "Enter Password";
"OwnershipTransfer.EnterPasswordText" = "Please enter your 2-Step Verification password to confirm the action.";
@@ -8359,10 +8359,10 @@ Sorry for the inconvenience.";
"Navigation.AllChats" = "All Chats";
"Group.Management.AntiSpam" = "Aggressive Anti-Spam";
-"Group.Management.AntiSpamInfo" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report false positives in Recent Actions.";
+"Group.Management.AntiSpamInfo" = "WinterGram will filter more spam but may occasionally affect ordinary messages. You can report false positives in Recent Actions.";
"Group.Management.AntiSpamMagic" = "magic";
-"Group.AdminLog.AntiSpamTitle" = "Telegram Anti-Spam";
+"Group.AdminLog.AntiSpamTitle" = "WinterGram Anti-Spam";
"Group.AdminLog.AntiSpamText" = "You can manage anti-spam settings in Group Info > [Administrators]().";
"ChatList.ThreadHideAction" = "Hide";
@@ -8386,7 +8386,7 @@ Sorry for the inconvenience.";
"DialogList.SearchSectionMessagesIn" = "Messages in %@";
"Conversation.ContextMenuReportFalsePositive" = "Report False Positive";
-"Group.AdminLog.AntiSpamFalsePositiveReportedText" = "Telegram moderators will review your report. Thank you!";
+"Group.AdminLog.AntiSpamFalsePositiveReportedText" = "WinterGram moderators will review your report. Thank you!";
"ChatList.EmptyTopicsDescription" = "Older messages from this group have been moved to \"General\".";
@@ -8549,7 +8549,7 @@ Sorry for the inconvenience.";
"Conversation.SuggestedPhotoTitle" = "Suggested Photo";
"Conversation.SuggestedPhotoText" = "**%@** suggests you to use this profile photo.";
-"Conversation.SuggestedPhotoTextExpanded" = "%@ suggests you to use this profile photo for your Telegram account.";
+"Conversation.SuggestedPhotoTextExpanded" = "%@ suggests you to use this profile photo for your WinterGram account.";
"Conversation.SuggestedPhotoTextYou" = "You suggested **%@** to use this profile photo.";
"Conversation.SuggestedPhotoView" = "View Photo";
"Conversation.SuggestedPhotoSuccess" = "Photo updated";
@@ -8557,7 +8557,7 @@ Sorry for the inconvenience.";
"Conversation.SuggestedVideoTitle" = "Suggested Video";
"Conversation.SuggestedVideoText" = "**%@** suggests you to use this profile video.";
-"Conversation.SuggestedVideoTextExpanded" = "%@ suggests you to use this profile video for your Telegram account.";
+"Conversation.SuggestedVideoTextExpanded" = "%@ suggests you to use this profile video for your WinterGram account.";
"Conversation.SuggestedVideoTextYou" = "You suggested **%@** to use this profile video.";
"Conversation.SuggestedVideoView" = "View Video";
"Conversation.SuggestedVideoSuccess" = "Video updated";
@@ -8638,9 +8638,9 @@ Sorry for the inconvenience.";
"StorageManagement.Title" = "Storage Usage";
"StorageManagement.TitleCleared" = "Storage Cleared";
-"StorageManagement.DescriptionCleared" = "All media can be re-downloaded from the Telegram cloud if you need it again.";
-"StorageManagement.DescriptionChatUsage" = "This chat uses %1$@% of your Telegram cache.";
-"StorageManagement.DescriptionAppUsage" = "Telegram uses %1$@% of your free disk space.";
+"StorageManagement.DescriptionCleared" = "All media can be re-downloaded from the WinterGram cloud if you need it again.";
+"StorageManagement.DescriptionChatUsage" = "This chat uses %1$@% of your WinterGram cache.";
+"StorageManagement.DescriptionAppUsage" = "WinterGram uses %1$@% of your free disk space.";
"StorageManagement.ClearAll" = "Clear Entire Cache";
"StorageManagement.ClearSelected" = "Clear Selected";
@@ -8654,7 +8654,7 @@ Sorry for the inconvenience.";
"StorageManagement.SectionAvatars" = "Avatars";
"StorageManagement.SectionMiscellaneous" = "Misc";
-"StorageManagement.SectionsDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
+"StorageManagement.SectionsDescription" = "All media will stay in the WinterGram cloud and can be re-downloaded if you need it again.";
"StorageManagement.AutoremoveHeader" = "AUTO-REMOVE CACHED MEDIA";
"StorageManagement.AutoremoveDescription" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.";
@@ -8791,26 +8791,26 @@ Sorry for the inconvenience.";
"AvatarEditor.Set" = "Set";
"AvatarEditor.Suggest" = "Suggest";
-"Premium.UpgradeDescription" = "Your current **Telegram Premium** plan can be upgraded at a **discount**.";
+"Premium.UpgradeDescription" = "Your current **WinterGram Premium** plan can be upgraded at a **discount**.";
"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.";
+"ChatList.PremiumAnnualDiscountTitle" = "WinterGram Premium with a discount of %@";
+"ChatList.PremiumAnnualDiscountText" = "Sign up for the annual payment plan for WinterGram Premium now to get the discount.";
"ChatList.PremiumAnnualUpgradeTitle" = "Save on your subscription up to %@";
-"ChatList.PremiumAnnualUpgradeText" = "Upgrade to the annual payment plan for Telegram Premium to enjoy the discount.";
+"ChatList.PremiumAnnualUpgradeText" = "Upgrade to the annual payment plan for WinterGram Premium to enjoy the discount.";
-"Premium.Emoji.Description" = "Unlock this emoji and many more by subscribing to Telegram Premium.";
+"Premium.Emoji.Description" = "Unlock this emoji and many more by subscribing to WinterGram Premium.";
"Premium.Emoji.Proceed" = "Unlock Premium Emoji";
"Localization.TranslateEntireChat" = "Translate Entire Chat";
"Premium.Translation" = "Real-Time Translation";
"Premium.TranslationInfo" = "Real-time translation of channels and chats into other languages.";
-"Premium.TranslationStandaloneInfo" = "Subscribe to **Telegram Premium** to be able to translate all messages in a chat at once.";
-"Premium.Translation.Proceed" = "About Telegram Premium";
+"Premium.TranslationStandaloneInfo" = "Subscribe to **WinterGram Premium** to be able to translate all messages in a chat at once.";
+"Premium.Translation.Proceed" = "About WinterGram Premium";
"Settings.PauseMusicOnRecording" = "Pause Music While Recoding";
@@ -8958,7 +8958,7 @@ Sorry for the inconvenience.";
"Login.CodeSentCallText" = "Calling **%@** to dictate the code.";
-"Premium.Purchase.OnlyOneSubscriptionAllowed" = "You have already purchased Telegram Premium for another account. You can only have one Telegram Premium subscription on one Apple ID.";
+"Premium.Purchase.OnlyOneSubscriptionAllowed" = "You have already purchased WinterGram Premium for another account. You can only have one WinterGram Premium subscription on one Apple ID.";
"Call.VoiceOver.Minimize" = "Minimize Call";
@@ -9047,7 +9047,7 @@ Sorry for the inconvenience.";
"PowerSavingScreen.OptionAutoplayEmojiText" = "Loop animated emoji in messages, reactions, statuses.";
"PowerSavingScreen.OptionAutoplayEffectsTitle" = "Interface Effects";
-"PowerSavingScreen.OptionAutoplayEffectsText" = "Various effects and animations that make Telegram look amazing.";
+"PowerSavingScreen.OptionAutoplayEffectsText" = "Various effects and animations that make WinterGram look amazing.";
"PowerSavingScreen.OptionBackgroundTitle" = "Extended Background Time";
"PowerSavingScreen.OptionBackgroundText" = "Update chats faster when switching between apps.";
@@ -9106,18 +9106,18 @@ Sorry for the inconvenience.";
"Attachment.Gift" = "Gift";
-"Premium.Gift.TitleShort" = "Telegram Premium";
+"Premium.Gift.TitleShort" = "WinterGram Premium";
-"VoiceOver.GiftPremium" = "Gift Telegram Premium";
+"VoiceOver.GiftPremium" = "Gift WinterGram Premium";
"Login.Email.CantAccess" = "Can't access this email?";
"Login.Email.ResetTitle" = "Reset Email";
-"Login.Email.ResetText" = "You can change your login email if you are logged into Telegram from another device. Otherwise, if you don't have access to email %1$@, you can reset this email with an **SMS** code **%2$@**.";
+"Login.Email.ResetText" = "You can change your login email if you are logged into WinterGram from another device. Otherwise, if you don't have access to email %1$@, you can reset this email with an **SMS** code **%2$@**.";
"Login.Email.Reset" = "Reset";
"Login.Email.ResetNowViaSMS" = "Reset now via SMS";
"Login.Email.WillBeResetIn" = "Email will be reset %@";
-"Login.Email.PremiumRequiredTitle" = "Telegram Premium Required";
-"Login.Email.PremiumRequiredText" = "Due to high cost of SMS in your country, you need to have a **Telegram Premium** account to reset this email via an SMS code.\n\nYou can ask a friend to gift a Premium subscription for your account\n**%@**";
+"Login.Email.PremiumRequiredTitle" = "WinterGram Premium Required";
+"Login.Email.PremiumRequiredText" = "Due to high cost of SMS in your country, you need to have a **WinterGram Premium** account to reset this email via an SMS code.\n\nYou can ask a friend to gift a Premium subscription for your account\n**%@**";
"Login.Email.ElapsedTime" = "in %@";
"Login.Email.ResetingNow" = "Please wait...";
@@ -9167,11 +9167,11 @@ Sorry for the inconvenience.";
"Wallpaper.ApplyForAll" = "Apply For All Chats";
"Wallpaper.ApplyForChat" = "Apply For This Chat";
-"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **Telegram Premium** to increase this limit up to **%2$@**.";
+"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **WinterGram Premium** to increase this limit up to **%2$@**.";
"Premium.MaxSharedFolderMembershipNoPremiumText" = "You can only add **%1$@** shareable folders. We are working to let you increase this limit in the future.";
"Premium.MaxSharedFolderMembershipFinalText" = "Sorry, you can only add **%1$@** shareable folders.";
-"Premium.MaxSharedFolderLinksText" = "You can only create **%1$@** invite links. Upgrade to **Telegram Premium** to increase the links limit to **%2$@**.";
+"Premium.MaxSharedFolderLinksText" = "You can only create **%1$@** invite links. Upgrade to **WinterGram Premium** to increase the links limit to **%2$@**.";
"Premium.MaxSharedFolderLinksNoPremiumText" = "You can only create **%1$@** invite links. We are working to let you increase this limit in the future.";
"Premium.MaxSharedFolderLinksFinalText" = "Sorry, you can only create **%1$@** invite links";
@@ -9189,7 +9189,7 @@ Sorry for the inconvenience.";
"Conversation.OpenChatFolder" = "VIEW CHAT LIST";
-"Premium.MaxChannelsText" = "You can only join **%1$@** groups and channels. Upgrade to **Telegram Premium** to increase the links limit to **%2$@**.";
+"Premium.MaxChannelsText" = "You can only join **%1$@** groups and channels. Upgrade to **WinterGram Premium** to increase the links limit to **%2$@**.";
"Premium.MaxChannelsNoPremiumText" = "You can only join **%1$@** groups and channels. We are working to let you increase this limit in the future.";
"Premium.MaxChannelsFinalText" = "Sorry, you can only join **%1$@** groups and channels";
@@ -9321,7 +9321,7 @@ Sorry for the inconvenience.";
"ChatListFilter.LinkActionDelete" = "Delete";
"InviteLink.QRCodeFolder.Title" = "Invite by QR Code";
-"InviteLink.QRCodeFolder.Text" = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link.";
+"InviteLink.QRCodeFolder.Text" = "Everyone on WinterGram can scan this code to add this folder and join the chats included in this invite link.";
"FolderLinkPreview.IconTabLeft" = "All Chats";
"FolderLinkPreview.IconTabRight" = "Personal";
@@ -9399,16 +9399,16 @@ Sorry for the inconvenience.";
"UserInfo.BotNamePlaceholder" = "Bot Name";
"ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off";
-"ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it.";
+"ChatList.PremiumRestoreDiscountText" = "Your WinterGram Premium has recently expired. Tap here to extend it.";
"Notification.LockScreenReactionPlaceholder" = "Reaction";
"UserInfo.BotNamePlaceholder" = "Bot Name";
"ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off";
-"ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it.";
+"ChatList.PremiumRestoreDiscountText" = "Your WinterGram Premium has recently expired. Tap here to extend it.";
-"Login.ErrorAppOutdated" = "Please update Telegram to the latest version to log in.";
+"Login.ErrorAppOutdated" = "Please update WinterGram to the latest version to log in.";
"Login.GetCodeViaFragment" = "Get a code via Fragment";
@@ -9459,8 +9459,8 @@ Sorry for the inconvenience.";
"ChatList.Archive.ContextInfo" = "How Does It Work?";
"ChatList.ContextSelectChats" = "Select Chats";
-"StoryFeed.TooltipPremiumPosting" = "Posting stories is currently available only\nto subscribers of [Telegram Premium]().";
-"StoryFeed.TooltipPremiumPostingLimited" = "This month, posting stories is only available to subscribers of [Telegram Premium]().";
+"StoryFeed.TooltipPremiumPosting" = "Posting stories is currently available only\nto subscribers of [WinterGram Premium]().";
+"StoryFeed.TooltipPremiumPostingLimited" = "This month, posting stories is only available to subscribers of [WinterGram Premium]().";
"StoryFeed.TooltipStoryLimitValue_1" = "1 story";
"StoryFeed.TooltipStoryLimitValue_any" = "%d stories";
"StoryFeed.TooltipStoryLimit" = "You can't post more than **%@** stories in **24 hours**.";
@@ -9498,7 +9498,7 @@ Sorry for the inconvenience.";
"ArchiveSettings.KeepArchived" = "Always Keep Archived";
"ArchiveSettings.AutomaticallyArchive" = "Automatically Archive";
-"ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [Telegram Premium]().";
+"ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [WinterGram Premium]().";
"NotificationSettings.Stories.ShowAll" = "Show All Notifications";
"NotificationSettings.Stories.ShowImportant" = "Show Important Notifications";
@@ -9577,8 +9577,8 @@ Sorry for the inconvenience.";
"Story.HeaderEdited" = "edited";
"Story.CaptionShowMore" = "Show more";
-"Story.UnsupportedText" = "This story is not supported by\nyour version of Telegram.";
-"Story.UnsupportedAction" = "Update Telegram";
+"Story.UnsupportedText" = "This story is not supported by\nyour version of WinterGram.";
+"Story.UnsupportedAction" = "Update WinterGram";
"Story.ScreenshotBlockedTitle" = "Screenshot Blocked";
"Story.ScreenshotBlockedText" = "The story you tried to take a\nscreenshot of is protected from\ncopying by its creator.";
@@ -9653,7 +9653,7 @@ Sorry for the inconvenience.";
"Story.Camera.SwipeLeftRelease" = "Release to lock";
"Story.Camera.SwipeRightToFlip" = "Swipe right to flip";
-"Story.Camera.AccessPlaceholderTitle" = "Allow Telegram to access your camera and microphone";
+"Story.Camera.AccessPlaceholderTitle" = "Allow WinterGram to access your camera and microphone";
"Story.Camera.AccessPlaceholderText" = "This lets you share photos and record videos.";
"Story.Camera.AccessOpenSettings" = "Open Settings";
@@ -9671,7 +9671,7 @@ Sorry for the inconvenience.";
"Story.Editor.ExpirationValue_1" = "1 Hour";
"Story.Editor.ExpirationValue_any" = "%d Hours";
-"Story.Editor.TooltipPremiumExpiration" = "Subscribe to [Telegram Premium]() to make your stories disappear after 6, 12 or 48 hours.";
+"Story.Editor.TooltipPremiumExpiration" = "Subscribe to [WinterGram Premium]() to make your stories disappear after 6, 12 or 48 hours.";
"Story.Editor.InputPlaceholderAddCaption" = "Add a caption...";
@@ -9771,11 +9771,11 @@ Sorry for the inconvenience.";
"Chat.OpenStory" = "OPEN STORY";
"Story.Editor.TooltipPremiumCaptionLimitTitle" = "Maximum Length Reached";
-"Story.Editor.TooltipPremiumCaptionLimitText" = "Increase this limit 10 times to 2048 symbols by subscribing to [Telegram Premium]().";
+"Story.Editor.TooltipPremiumCaptionLimitText" = "Increase this limit 10 times to 2048 symbols by subscribing to [WinterGram Premium]().";
-"Story.Editor.TooltipPremiumCaptionEntities" = "Subscribe to [Telegram Premium]() to add links and formatting in captions to your stories.";
+"Story.Editor.TooltipPremiumCaptionEntities" = "Subscribe to [WinterGram Premium]() to add links and formatting in captions to your stories.";
-"Story.Context.TooltipPremiumSaveStories" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery.";
+"Story.Context.TooltipPremiumSaveStories" = "Subscribe to [WinterGram Premium]() to save other people's unprotected stories to your Gallery.";
"Story.Privacy.GroupTooLarge" = "Group Too Large";
"Story.Privacy.GroupParticipantsLimit" = "You can select groups that are up to 200 members.";
@@ -9836,11 +9836,11 @@ Sorry for the inconvenience.";
"Premium.Stories.Format.Title" = "Links and Formatting";
"Premium.Stories.Format.Text" = "Add links and formatting in captions to your stories.";
-"Premium.MaxStoriesWeeklyText" = "You can post **%@** stories in a week. Upgrade to **Telegram Premium** to increase this limit to **%@**.";
+"Premium.MaxStoriesWeeklyText" = "You can post **%@** stories in a week. Upgrade to **WinterGram Premium** to increase this limit to **%@**.";
"Premium.MaxStoriesWeeklyNoPremiumText" = "You have reached the limit of **%@** stories per week.";
"Premium.MaxStoriesWeeklyFinalText" = "You have reached the limit of **%@** stories per week.";
-"Premium.MaxStoriesMonthlyText" = "You can post **%@** stories in a month. Upgrade to **Telegram Premium** to increase this limit to **%@**.";
+"Premium.MaxStoriesMonthlyText" = "You can post **%@** stories in a month. Upgrade to **WinterGram Premium** to increase this limit to **%@**.";
"Premium.MaxStoriesMonthlyNoPremiumText" = "You have reached the limit of **%@** stories per month.";
"Premium.MaxStoriesMonthlyFinalText" = "You have reached the limit of **%@** stories per month.";
@@ -9856,7 +9856,7 @@ Sorry for the inconvenience.";
"Story.ContextDeleteContact" = "Delete Contact";
"Story.ToastDeletedContact" = "**%@** has been removed from your contacts.";
"Story.ToastUserBlocked" = "**%@** has been blocked.";
-"Story.ToastPremiumSaveToGallery" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery.";
+"Story.ToastPremiumSaveToGallery" = "Subscribe to [WinterGram Premium]() to save other people's unprotected stories to your Gallery.";
"Story.PremiumUpgradeStoriesButton" = "Upgrade Stories";
"Story.ContextStealthMode" = "Stealth Mode";
"Story.AlertStealthModeActiveTitle" = "You are in Stealth Mode now";
@@ -9867,9 +9867,9 @@ Sorry for the inconvenience.";
"Story.ToastStealthModeActivatedTitle" = "Stealth Mode On";
"Story.ToastStealthModeActivatedText" = "The creators of stories you viewed in the last **%1$@** or will view in the next **%2$@** won’t see you in the viewers’ lists.";
-"Story.ViewList.PremiumUpgradeText" = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]().";
+"Story.ViewList.PremiumUpgradeText" = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [WinterGram Premium]().";
"Story.ViewList.PremiumUpgradeAction" = "Learn More";
-"Story.ViewList.PremiumUpgradeInlineText" = "To unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]().";
+"Story.ViewList.PremiumUpgradeInlineText" = "To unlock viewers' lists for expired and saved stories, subscribe to [WinterGram Premium]().";
"Story.ViewList.NotFullyRecorded" = "Information about the other viewers wasn’t recorded.";
"Story.ViewList.EmptyTextSearch" = "No views found";
"Story.ViewList.EmptyTextContacts" = "None of your contacts viewed this story.";
@@ -9885,7 +9885,7 @@ Sorry for the inconvenience.";
"Story.Footer.ViewCount_any" = "|%d| Views";
"Story.StealthMode.Title" = "Stealth Mode";
"Story.StealthMode.ControlText" = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them.";
-"Story.StealthMode.UpgradeText" = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them.";
+"Story.StealthMode.UpgradeText" = "Subscribe to WinterGram Premium to hide the fact that you viewed peoples' stories from them.";
"Story.StealthMode.RecentTitle" = "Hide Recent Views";
"Story.StealthMode.RecentText" = "Hide my views in the last **%@**.";
"Story.StealthMode.NextTitle" = "Hide Next Views";
@@ -9913,7 +9913,7 @@ Sorry for the inconvenience.";
"Story.Privacy.KeepOnChannelPage" = "Post to Channel Profile";
"Story.Privacy.KeepOnChannelPageInfo" = "Keep this story on channel profile even after it expires in %@.";
-"Story.Editor.TooltipPremiumReaction" = "Subscribe to [Telegram Premium]() to use this reaction.";
+"Story.Editor.TooltipPremiumReaction" = "Subscribe to [WinterGram Premium]() to use this reaction.";
"Story.Privacy.TooltipStoryArchivedChannel" = "Users will see this story on the channel page even after it expires.";
@@ -9938,7 +9938,7 @@ Sorry for the inconvenience.";
"MediaPicker.Timer.Video.KeepTooltip" = "Video will be kept in chat.";
"WebApp.AllowWriteTitle" = "Allow Sending Messages?";
-"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram.";
+"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on WinterGram.";
"WebApp.SharePhoneTitle" = "Share Phone Number?";
"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services.";
@@ -9948,7 +9948,7 @@ Sorry for the inconvenience.";
"Story.Editor.TooltipPremiumReactionLimitValue_any" = "**%@** reactions tags";
"Story.Editor.TooltipPremiumReactionLimitTitle" = "Increase Limit";
-"Story.Editor.TooltipPremiumReactionLimitText" = "Upgrade to [Telegram Premium]() to add up to %@ to a story.";
+"Story.Editor.TooltipPremiumReactionLimitText" = "Upgrade to [WinterGram Premium]() to add up to %@ to a story.";
"Story.Editor.TooltipReachedReactionLimitTitle" = "Limit Reached";
"Story.Editor.TooltipReachedReactionLimitText" = "You can't add up more than %@ to a story.";
@@ -9957,7 +9957,7 @@ Sorry for the inconvenience.";
"Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once.";
"WebApp.DisclaimerTitle" = "Terms of Use";
-"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue.";
+"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with WinterGram. You must agree to the Terms of Use of mini apps to continue.";
"WebApp.DisclaimerAgree" = "I agree to the [Terms of Use]()";
"WebApp.DisclaimerContinue" = "Continue";
"WebApp.Disclaimer_URL" = "https://telegram.org/tos/mini-apps";
@@ -9966,7 +9966,7 @@ Sorry for the inconvenience.";
"WebApp.ShortcutsSettingsAdded" = "**%@** shortcut added in attachment menu and Settings.";
"WebApp.AllowWriteTitle" = "Allow Sending Messages?";
-"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram.";
+"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on WinterGram.";
"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@.";
@@ -10005,7 +10005,7 @@ Sorry for the inconvenience.";
"ChatList.SessionReview.PanelConfirm" = "Yes, it's me";
"ChatList.SessionReview.PanelReject" = "No, it's not me!";
-"SessionReview.NoticeText" = "Never send your login code to anyone or you can lose your Telegram account!";
+"SessionReview.NoticeText" = "Never send your login code to anyone or you can lose your WinterGram account!";
"SessionReview.Title" = "New Login Prevented";
"SessionReview.Text" = "We have terminated the login attempt from **%1$@**, **%2$@**";
"SessionReview.OkAction" = "Got it";
@@ -10112,9 +10112,9 @@ Sorry for the inconvenience.";
"ChannelBoost.Error.BoostTooOftenTitle" = "Can't Boost Too Often";
"ChannelBoost.Error.BoostTooOftenText" = "You can change the channel you boost only once a day. Next time you can boost is in **%@**.";
"ChannelBoost.Error.PremiumNeededTitle" = "Premium Needed";
-"ChannelBoost.Error.PremiumNeededText" = "Only **Telegram Premium** subscribers can boost channels. Do you want to subscribe to **Telegram Premium**?";
+"ChannelBoost.Error.PremiumNeededText" = "Only **WinterGram Premium** subscribers can boost channels. Do you want to subscribe to **WinterGram Premium**?";
"ChannelBoost.Error.GiftedPremiumNotAllowedTitle" = "Can't Boost with Gifted Premium";
-"ChannelBoost.Error.GiftedPremiumNotAllowedText" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels.";
+"ChannelBoost.Error.GiftedPremiumNotAllowedText" = "Because your **WinterGram Premium** subscription was gifted to you, you can't use it to boost channels.";
"Chat.ErrorFolderLinkExpired" = "The folder link has expired.";
@@ -10148,7 +10148,7 @@ Sorry for the inconvenience.";
"Notification.GiftLink" = "You received a gift";
-"MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of Telegram Premium";
+"MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of WinterGram Premium";
"MESSAGE_GIVEAWAY" = "%1$@ sent you a giveaway of %2$@x %3$@mo Premium subscriptions";
"CHANNEL_MESSAGE_GIVEAWAY" = "%1$@ posted a giveaway of %2$@x %3$@mo Premium subscriptions";
"CHAT_MESSAGE_GIVEAWAY" = "%1$@ sent a giveaway of %3$@x %4$@mo Premium subscriptions to the group %2$@";
@@ -10156,7 +10156,7 @@ Sorry for the inconvenience.";
"REACT_GIVEAWAY" = "%1$@ reacted %2$@ to your giveaway";
"CHAT_REACT_GIVEAWAY" = "%1$@ reacted %3$@ in group %2$@ to your giveaway";
-"Notification.GiveawayStarted" = "%1$@ just started a giveaway of Telegram Premium subscriptions for its followers.";
+"Notification.GiveawayStarted" = "%1$@ just started a giveaway of WinterGram Premium subscriptions for its followers.";
"Appearance.NameColor" = "Your Name Color";
@@ -10168,7 +10168,7 @@ Sorry for the inconvenience.";
"NameColor.ChatPreview.ReplyText.Channel" = "Reply to your channel message";
"NameColor.ChatPreview.MessageText.Account" = "Your name and replies to your messages will be shown in the selected color.";
"NameColor.ChatPreview.MessageText.Channel" = "The name of your channek and replies to its messages will be shown in the selected color.";
-"NameColor.ChatPreview.LinkSite" = "Telegram";
+"NameColor.ChatPreview.LinkSite" = "WinterGram";
"NameColor.ChatPreview.LinkTitle" = "Link Preview";
"NameColor.ChatPreview.LinkText" = "Your selected color will also tint the link preview.";
"NameColor.ChatPreview.Description.Account" = "You can choose an individual color to tint your name, the links you send, and replies to your messages.";
@@ -10177,7 +10177,7 @@ Sorry for the inconvenience.";
"NameColor.ApplyColor" = "Apply Color";
"NameColor.ApplyColorAndBackgroundEmoji" = "Apply Color and Icon";
-"NameColor.TooltipPremium.Account" = "Subscribe to [Telegram Premium]() to choose a custom color for your name.";
+"NameColor.TooltipPremium.Account" = "Subscribe to [WinterGram Premium]() to choose a custom color for your name.";
"NameColor.BackgroundEmoji.Title" = "ADD ICONS TO REPLIES";
"NameColor.BackgroundEmoji.Remove" = "REMOVE ICON";
@@ -10194,8 +10194,8 @@ Sorry for the inconvenience.";
"Chat.ErrorQuoteOutdatedText" = "**%@** updated the message you are quoting. Edit your quote to make it up-to-date.";
"Chat.ErrorQuoteOutdatedActionEdit" = "Edit";
-"Premium.BoostByGiftDescription" = "Boost your channel by gifting your subscribers Telegram Premium. [Get boosts >]()";
-"Premium.BoostByGiftDescription2" = "Boost your channel by gifting your subscribers Telegram Premium. [Get boosts >]()";
+"Premium.BoostByGiftDescription" = "Boost your channel by gifting your subscribers WinterGram Premium. [Get boosts >]()";
+"Premium.BoostByGiftDescription2" = "Boost your channel by gifting your subscribers WinterGram Premium. [Get boosts >]()";
"ChatContextMenu.QuoteSelectionTip" = "Hold on a word, then move cursor to select more| text to quote.";
@@ -10246,11 +10246,11 @@ Sorry for the inconvenience.";
"Stats.Boosts.ShowMoreBoosts_any" = "Show %@ More Boosts";
"ReassignBoost.Title" = "Reassign Boosts";
-"ReassignBoost.Description" = "To boost **%1$@**, reassign a previous boost or gift **Telegram Premium** to a friend to get **%2$@** additional boosts.";
+"ReassignBoost.Description" = "To boost **%1$@**, reassign a previous boost or gift **WinterGram Premium** to a friend to get **%2$@** additional boosts.";
"ReassignBoost.ReassignBoosts" = "Reassign Boosts";
"ReassignBoost.AvailableIn" = "Available in %@";
"ReassignBoost.ExpiresOn" = "Boost expires on %@";
-"ReassignBoost.WaitForCooldown" = "Wait until the boost is available or get **%1$@** more boosts by gifting a **Telegram Premium** subscription.";
+"ReassignBoost.WaitForCooldown" = "Wait until the boost is available or get **%1$@** more boosts by gifting a **WinterGram Premium** subscription.";
"ReassignBoost.Success" = "%1$@ from %2$@.";
"ReassignBoost.Boosts_1" = "%@ boost is reassigned";
@@ -10263,14 +10263,14 @@ Sorry for the inconvenience.";
"ReassignBoost.OtherGroupsAndChannels_any" = "%@ other groups and channels";
"ChannelBoost.MoreBoosts.Title" = "More Boosts Needed";
-"ChannelBoost.MoreBoosts.Text" = "To boost **%1$@** again, gift **Telegram Premium** to a friend and get **%2$@** additional boosts.";
+"ChannelBoost.MoreBoosts.Text" = "To boost **%1$@** again, gift **WinterGram Premium** to a friend and get **%2$@** additional boosts.";
"ChannelBoost.MoreBoosts.Gift" = "Gift Premium";
"BoostGift.Title" = "Boosts via Gifts";
"BoostGift.Description" = "Get more boosts for your channel by gifting\nPremium to your subscribers.";
"BoostGift.PrepaidGiveawayTitle" = "PREPAID GIVEAWAY";
-"BoostGift.PrepaidGiveawayCount_1" = "%@ Telegram Premium";
-"BoostGift.PrepaidGiveawayCount_any" = "%@ Telegram Premium";
+"BoostGift.PrepaidGiveawayCount_1" = "%@ WinterGram Premium";
+"BoostGift.PrepaidGiveawayCount_any" = "%@ WinterGram Premium";
"BoostGift.PrepaidGiveawayMonths" = "%@-month subscriptions";
"BoostGift.CreateGiveaway" = "Create Giveaway";
"BoostGift.CreateGiveawayInfo" = "winners are chosen randomly";
@@ -10296,11 +10296,11 @@ Sorry for the inconvenience.";
"BoostGift.LimitSubscribersInfo" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started.";
"BoostGift.DateTitle" = "DATE WHEN GIVEAWAY ENDS";
"BoostGift.DateEnds" = "Ends";
-"BoostGift.DateInfo" = "Choose when %1$@ of your channel will be randomly selected to receive Telegram Premium.";
+"BoostGift.DateInfo" = "Choose when %1$@ of your channel will be randomly selected to receive WinterGram Premium.";
"BoostGift.DateInfoSubscribers_1" = "%@ subscriber";
"BoostGift.DateInfoSubscribers_any" = "%@ subscribers";
"BoostGift.DurationTitle" = "DURATION OF PREMIUM SUBSCRIPTIONS";
-"BoostGift.PremiumInfo" = "You can review the list of features and terms of use for Telegram Premium [here]().";
+"BoostGift.PremiumInfo" = "You can review the list of features and terms of use for WinterGram Premium [here]().";
"BoostGift.GiftPremium" = "Gift Premium";
"BoostGift.StartGiveaway" = "Start Giveaway";
"BoostGift.ReduceQuantity.Title" = "Reduce Quantity";
@@ -10333,8 +10333,8 @@ Sorry for the inconvenience.";
"BoostGift.Channels.Save" = "Save Channels";
"Stats.Boosts.PrepaidGiveawaysTitle" = "PREPAID GIVEAWAYS";
-"Stats.Boosts.PrepaidGiveawayCount_1" = "%@ Telegram Premium";
-"Stats.Boosts.PrepaidGiveawayCount_any" = "%@ Telegram Premiums";
+"Stats.Boosts.PrepaidGiveawayCount_1" = "%@ WinterGram Premium";
+"Stats.Boosts.PrepaidGiveawayCount_any" = "%@ WinterGram Premiums";
"Stats.Boosts.PrepaidGiveawayMonths" = "%@-month subscriptions";
"Stats.Boosts.PrepaidGiveawaysInfo" = "Select a giveaway you already paid for to set it up.";
"Stats.Boosts.ShortMonth" = "%@m";
@@ -10351,9 +10351,9 @@ Sorry for the inconvenience.";
"Stats.Boosts.TooltipToBeDistributed" = "The recipient will be selected when the giveaway ends.";
"Notification.PremiumPrize.Title" = "Congratulations!";
-"Notification.PremiumPrize.UnclaimedText" = "You have an unclaimed prize from a giveaway by **%1$@**.\n\nThis prize is a **Telegram Premium** subscription for %2$@.";
-"Notification.PremiumPrize.GiveawayText" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is a **Telegram Premium** subscription for %2$@.";
-"Notification.PremiumPrize.GiftText" = "You've received a gift from **%1$@**.\n\nYour gift is a **Telegram Premium** subscription for %2$@.";
+"Notification.PremiumPrize.UnclaimedText" = "You have an unclaimed prize from a giveaway by **%1$@**.\n\nThis prize is a **WinterGram Premium** subscription for %2$@.";
+"Notification.PremiumPrize.GiveawayText" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is a **WinterGram Premium** subscription for %2$@.";
+"Notification.PremiumPrize.GiftText" = "You've received a gift from **%1$@**.\n\nYour gift is a **WinterGram Premium** subscription for %2$@.";
"Notification.PremiumPrize.Months_1" = "**%@** month";
"Notification.PremiumPrize.Months_any" = "**%@** months";
"Notification.PremiumPrize.View" = "Open Gift Link";
@@ -10376,15 +10376,15 @@ Sorry for the inconvenience.";
"Chat.Giveaway.Info.EndedTitle" = "Giveaway Ended";
"Chat.Giveaway.Info.AlmostOver" = "The giveaway is almost over.";
"Chat.Giveaway.Info.OngoingIntro" = "The giveaway is sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers.";
-"Chat.Giveaway.Info.OngoingNewMany" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** and %4$@ after **%5$@**.";
-"Chat.Giveaway.Info.OngoingNew" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** after **%4$@**.";
-"Chat.Giveaway.Info.OngoingMany" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@** and %4$@.";
-"Chat.Giveaway.Info.Ongoing" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@**.";
+"Chat.Giveaway.Info.OngoingNewMany" = "On **%1$@**, WinterGram will automatically select %2$@ that joined **%3$@** and %4$@ after **%5$@**.";
+"Chat.Giveaway.Info.OngoingNew" = "On **%1$@**, WinterGram will automatically select %2$@ that joined **%3$@** after **%4$@**.";
+"Chat.Giveaway.Info.OngoingMany" = "On **%1$@**, WinterGram will automatically select %2$@ of **%3$@** and %4$@.";
+"Chat.Giveaway.Info.Ongoing" = "On **%1$@**, WinterGram will automatically select %2$@ of **%3$@**.";
"Chat.Giveaway.Info.EndedIntro" = "The giveaway was sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers.";
-"Chat.Giveaway.Info.EndedNewMany" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** and other listed channels after **%4$@**.";
-"Chat.Giveaway.Info.EndedNew" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** after **%4$@**.";
-"Chat.Giveaway.Info.EndedMany" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@** and other listed channels.";
-"Chat.Giveaway.Info.Ended" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@**.";
+"Chat.Giveaway.Info.EndedNewMany" = "On **%1$@**, WinterGram automatically selected %2$@ that joined **%3$@** and other listed channels after **%4$@**.";
+"Chat.Giveaway.Info.EndedNew" = "On **%1$@**, WinterGram automatically selected %2$@ that joined **%3$@** after **%4$@**.";
+"Chat.Giveaway.Info.EndedMany" = "On **%1$@**, WinterGram automatically selected %2$@ of **%3$@** and other listed channels.";
+"Chat.Giveaway.Info.Ended" = "On **%1$@**, WinterGram automatically selected %2$@ of **%3$@**.";
"Chat.Giveaway.Info.NotAllowedJoinedEarly" = "You are not eligible to participate in this giveaway, because you joined this channel on **%@**, which is before the contest started.";
"Chat.Giveaway.Info.NotAllowedAdmin" = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**%@**).";
"Chat.Giveaway.Info.NotAllowedCountry" = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway.";
@@ -10394,8 +10394,8 @@ Sorry for the inconvenience.";
"Chat.Giveaway.Info.ParticipatingMany" = "You are participating in this giveaway, because you have joined the channel **%1$@** (and %2$@).";
"Chat.Giveaway.Info.OtherChannels_1" = "**%@** other listed channel";
"Chat.Giveaway.Info.OtherChannels_any" = "**%@** other listed channels";
-"Chat.Giveaway.Info.Subscriptions_1" = "**%@ Telegram Premium** subscription";
-"Chat.Giveaway.Info.Subscriptions_any" = "**%@ Telegram Premium** subscriptions";
+"Chat.Giveaway.Info.Subscriptions_1" = "**%@ WinterGram Premium** subscription";
+"Chat.Giveaway.Info.Subscriptions_any" = "**%@ WinterGram Premium** subscriptions";
"Chat.Giveaway.Info.RandomUsers_1" = "**%@** random user";
"Chat.Giveaway.Info.RandomUsers_any" = "**%@** random user";
"Chat.Giveaway.Info.RandomSubscribers_1" = "**%@** random subscriber";
@@ -10421,10 +10421,10 @@ Sorry for the inconvenience.";
"Chat.Giveaway.Message.PrizeTitle" = "Giveaway Prizes";
"Chat.Giveaway.Message.PrizeText" = "%1$@ for %2$@.";
-"Chat.Giveaway.Message.Subscriptions_1" = "**%@** Telegram Premium Subscription";
-"Chat.Giveaway.Message.Subscriptions_any" = "**%@** Telegram Premium Subscriptions";
-"Chat.Giveaway.Message.WithSubscriptions_1" = "**%@** Telegram Premium Subscription";
-"Chat.Giveaway.Message.WithSubscriptions_any" = "**%@** Telegram Premium Subscriptions";
+"Chat.Giveaway.Message.Subscriptions_1" = "**%@** WinterGram Premium Subscription";
+"Chat.Giveaway.Message.Subscriptions_any" = "**%@** WinterGram Premium Subscriptions";
+"Chat.Giveaway.Message.WithSubscriptions_1" = "**%@** WinterGram Premium Subscription";
+"Chat.Giveaway.Message.WithSubscriptions_any" = "**%@** WinterGram Premium Subscriptions";
"Chat.Giveaway.Message.Months_1" = "**%@** month";
"Chat.Giveaway.Message.Months_any" = "**%@** months";
"Chat.Giveaway.Message.ParticipantsTitle" = "Participants";
@@ -10443,11 +10443,11 @@ Sorry for the inconvenience.";
"GiftLink.Title" = "Gift Link";
"GiftLink.UsedTitle" = "Used Gift Link";
-"GiftLink.Description" = "This link allows you to activate a **Telegram Premium** subscription.";
-"GiftLink.UsedDescription" = "This link was used to activate a **Telegram Premium** subscription.";
-"GiftLink.PersonalDescription" = "This link allows **%@** to activate a **Telegram Premium** subscription.";
-"GiftLink.PersonalUsedDescription" = "This link allowed **%@** to activate a **Telegram Premium** subscription.";
-"GiftLink.UnclaimedDescription" = "This link allows anyone to activate a **Telegram Premium** subscription.";
+"GiftLink.Description" = "This link allows you to activate a **WinterGram Premium** subscription.";
+"GiftLink.UsedDescription" = "This link was used to activate a **WinterGram Premium** subscription.";
+"GiftLink.PersonalDescription" = "This link allows **%@** to activate a **WinterGram Premium** subscription.";
+"GiftLink.PersonalUsedDescription" = "This link allowed **%@** to activate a **WinterGram Premium** subscription.";
+"GiftLink.UnclaimedDescription" = "This link allows anyone to activate a **WinterGram Premium** subscription.";
"GiftLink.Footer" = "You can also [send this link]() to a friend as a gift.";
"GiftLink.UsedFooter" = "This link was used on %@.";
"GiftLink.NotUsedFooter" = "This link hasn't been used yet.";
@@ -10461,8 +10461,8 @@ Sorry for the inconvenience.";
"GiftLink.Reason.Unclaimed" = "Incomplete Giveaway";
"GiftLink.Date" = "Date";
"GiftLink.NoRecipient" = "No recipient";
-"GiftLink.TelegramPremium_1" = "Telegram Premium for %@ month";
-"GiftLink.TelegramPremium_any" = "Telegram Premium for %@ months";
+"GiftLink.TelegramPremium_1" = "WinterGram Premium for %@ month";
+"GiftLink.TelegramPremium_any" = "WinterGram Premium for %@ months";
"GiftLink.LinkHidden" = "Only the recipient can see the link.";
"ChannelBoost.EnableColors" = "Enable Colors";
@@ -10521,18 +10521,18 @@ Sorry for the inconvenience.";
"Conversation.FreeTranscriptionCooldownTooltip_1" = "You have used all your **%@** free transcription this week.";
"Conversation.FreeTranscriptionCooldownTooltip_any" = "You have used all your **%@** free transcriptions this week.";
-"Conversation.FreeTranscriptionWaitOrSubscribe" = "Wait until **%@** to use it again or subscribe to [Telegram Premium]() now.";
+"Conversation.FreeTranscriptionWaitOrSubscribe" = "Wait until **%@** to use it again or subscribe to [WinterGram Premium]() now.";
-"Notification.GiveawayResults_1" = "**%@** winner of the giveaway was randomly selected by Telegram and received their gift link in a private message.";
-"Notification.GiveawayResults_any" = "**%@** winners of the giveaway were randomly selected by Telegram and received their gift links in private messages.";
+"Notification.GiveawayResults_1" = "**%@** winner of the giveaway was randomly selected by WinterGram and received their gift link in a private message.";
+"Notification.GiveawayResults_any" = "**%@** winners of the giveaway were randomly selected by WinterGram and received their gift links in private messages.";
-"Notification.GiveawayResultsNoWinners_1" = "Due to the giveaway terms, no winners could be selected by Telegram, a gift link was forwarded to channel administrators.";
-"Notification.GiveawayResultsNoWinners_any" = "Due to the giveaway terms, no winners could be selected by Telegram, all **%@** gift links were forwarded to channel administrators.";
-"Notification.GiveawayResultsNoWinners.Group_1" = "Due to the giveaway terms, no winners could be selected by Telegram, a gift link was forwarded to group administrators.";
-"Notification.GiveawayResultsNoWinners.Group_any" = "Due to the giveaway terms, no winners could be selected by Telegram, all **%@** gift links were forwarded to group administrators.";
+"Notification.GiveawayResultsNoWinners_1" = "Due to the giveaway terms, no winners could be selected by WinterGram, a gift link was forwarded to channel administrators.";
+"Notification.GiveawayResultsNoWinners_any" = "Due to the giveaway terms, no winners could be selected by WinterGram, all **%@** gift links were forwarded to channel administrators.";
+"Notification.GiveawayResultsNoWinners.Group_1" = "Due to the giveaway terms, no winners could be selected by WinterGram, a gift link was forwarded to group administrators.";
+"Notification.GiveawayResultsNoWinners.Group_any" = "Due to the giveaway terms, no winners could be selected by WinterGram, all **%@** gift links were forwarded to group administrators.";
-"Notification.GiveawayResultsMixedWinners_1" = "**%@** winner of the giveaway was randomly selected by Telegram and received their gift link in a private message.";
-"Notification.GiveawayResultsMixedWinners_any" = "**%@** winners of the giveaway were randomly selected by Telegram and received their gift links in private messages.";
+"Notification.GiveawayResultsMixedWinners_1" = "**%@** winner of the giveaway was randomly selected by WinterGram and received their gift link in a private message.";
+"Notification.GiveawayResultsMixedWinners_any" = "**%@** winners of the giveaway were randomly selected by WinterGram and received their gift links in private messages.";
"Notification.GiveawayResultsMixedUnclaimed_1" = "**%@** undistributed gift link was forwarded to channel administrators";
"Notification.GiveawayResultsMixedUnclaimed_any" = "**%@** undistributed gift links were forwarded to channel administrators";
"Notification.GiveawayResultsMixedUnclaimed.Group_1" = "**%@** undistributed gift link was forwarded to group administrators";
@@ -10549,9 +10549,9 @@ Sorry for the inconvenience.";
"Wallpaper.ApplyForMe" = "Apply for Me";
"Wallpaper.ApplyForBoth" = "Apply for Me and %@";
-"Premium.VoiceToText.Proceed" = "About Telegram Premium";
-"Premium.Wallpaper.Proceed" = "About Telegram Premium";
-"Premium.Colors.Proceed" = "About Telegram Premium";
+"Premium.VoiceToText.Proceed" = "About WinterGram Premium";
+"Premium.Wallpaper.Proceed" = "About WinterGram Premium";
+"Premium.Colors.Proceed" = "About WinterGram Premium";
"Notification.YouChangedWallpaperBoth" = "You set a new wallpaper for %@ and you.";
@@ -10596,7 +10596,7 @@ Sorry for the inconvenience.";
"Stats.ReactionsPerStory" = "Reactions Per Story";
"Channel.SimilarChannels.ShowMore" = "Show More Channels";
-"Channel.SimilarChannels.ShowMoreInfo" = "Subscribe to [Telegram Premium]()\nto unlock up to **100** similar channels.";
+"Channel.SimilarChannels.ShowMoreInfo" = "Subscribe to [WinterGram Premium]()\nto unlock up to **100** similar channels.";
"Premium.Colors" = "Name and Profile Colors";
"Premium.ColorsInfo" = "Choose a color and logo for your profile and replies to your messages.";
@@ -10607,7 +10607,7 @@ Sorry for the inconvenience.";
"MediaEditor.VideoRemovalConfirmation" = "Are you sure you want to delete video message?";
"MediaEditor.HoldToRecordVideo" = "Hold to record video";
-"Chat.ChannelRecommendation.PremiumTooltip" = "Subcribe to [Telegram Premium]() to unlock up to **100** channels.";
+"Chat.ChannelRecommendation.PremiumTooltip" = "Subcribe to [WinterGram Premium]() to unlock up to **100** channels.";
"Story.ForwardAuthorHiddenTooltip" = "The account was hidden by the user";
@@ -10652,7 +10652,7 @@ Sorry for the inconvenience.";
"Chat.InputPlaceholderReplyInTopic" = "Reply in %1$@";
"Chat.InputPlaceholderMessageInTopic" = "Message in %1$@";
-"Chat.Reactions.MultiplePremiumTooltip" = "You can set multiple reactions with [Telegram Premium]().";
+"Chat.Reactions.MultiplePremiumTooltip" = "You can set multiple reactions with [WinterGram Premium]().";
"Notification.Wallpaper.Remove" = "Remove";
"Chat.RemoveWallpaper.Title" = "Remove Wallpaper";
@@ -10672,11 +10672,11 @@ Sorry for the inconvenience.";
"BoostGift.AdditionalPrizesInfoOff" = "Turn this on if you want to give the winners your own prizes in addition to Premium subscriptions.";
"BoostGift.AdditionalPrizesInfoOn" = "All prizes: **%1$@** %2$@ %3$@.";
-"BoostGift.AdditionalPrizesInfoSubscriptions_1" = "%@ Telegram Premium subscription";
-"BoostGift.AdditionalPrizesInfoSubscriptions_any" = "%@ Telegram Premium subscriptions";
+"BoostGift.AdditionalPrizesInfoSubscriptions_1" = "%@ WinterGram Premium subscription";
+"BoostGift.AdditionalPrizesInfoSubscriptions_any" = "%@ WinterGram Premium subscriptions";
-"BoostGift.AdditionalPrizesInfoWithSubscriptions_1" = "with %@ Telegram Premium subscription";
-"BoostGift.AdditionalPrizesInfoWithSubscriptions_any" = "with %@ Telegram Premium subscriptions";
+"BoostGift.AdditionalPrizesInfoWithSubscriptions_1" = "with %@ WinterGram Premium subscription";
+"BoostGift.AdditionalPrizesInfoWithSubscriptions_any" = "with %@ WinterGram Premium subscriptions";
"BoostGift.AdditionalPrizesInfoForMonths_1" = "for **%@** month";
"BoostGift.AdditionalPrizesInfoForMonths_any" = "for **%@** months";
@@ -10688,8 +10688,8 @@ Sorry for the inconvenience.";
"Chat.Giveaway.Message.WinnersSelectedTitle.One" = "Winner Selected!";
"Chat.Giveaway.Message.WinnersSelectedTitle.Many" = "Winners Selected!";
-"Chat.Giveaway.Message.WinnersSelectedText_1" = "**%@** winner of the [Giveaway]() was randomly selected by Telegram.";
-"Chat.Giveaway.Message.WinnersSelectedText_any" = "**%@** winners of the [Giveaway]() were randomly selected by Telegram.";
+"Chat.Giveaway.Message.WinnersSelectedText_1" = "**%@** winner of the [Giveaway]() was randomly selected by WinterGram.";
+"Chat.Giveaway.Message.WinnersSelectedText_any" = "**%@** winners of the [Giveaway]() were randomly selected by WinterGram.";
"Chat.Giveaway.Message.WinnersTitle.One" = "Winner";
"Chat.Giveaway.Message.WinnersTitle.Many" = "Winners";
@@ -10704,7 +10704,7 @@ Sorry for the inconvenience.";
"Story.ViewList.ContextSortChannelInfo" = "Choose the order for the list of reactions.";
-"Premium.Gift.MultipleDescription" = "Give %1$@%2$@ access to exclusive features with **Telegram Premium**.";
+"Premium.Gift.MultipleDescription" = "Give %1$@%2$@ access to exclusive features with **WinterGram Premium**.";
"Premium.Gift.NamesAndMore_1" = " and %@ more";
"Premium.Gift.NamesAndMore_any" = " and %@ more";
@@ -10727,13 +10727,13 @@ Sorry for the inconvenience.";
"Premium.Gift.GiftMultipleSubscriptions_1" = "Gift %@ Subscription";
"Premium.Gift.GiftMultipleSubscriptions_any" = "Gift %@ Subscriptions";
-"Message.GiveawayEndedWinners_1" = "%@ winner of the giveaway was randomly selected by Telegram";
-"Message.GiveawayEndedWinners_any" = "%@ winners of the giveaway was randomly selected by Telegram";
-"Message.GiveawayEndedNoWinners" = "Due to the giveaway terms, no winners could be selected by Telegram";
+"Message.GiveawayEndedWinners_1" = "%@ winner of the giveaway was randomly selected by WinterGram";
+"Message.GiveawayEndedWinners_any" = "%@ winners of the giveaway was randomly selected by WinterGram";
+"Message.GiveawayEndedNoWinners" = "Due to the giveaway terms, no winners could be selected by WinterGram";
"Story.ViewMessage" = "View Message";
-"Premium.Gift.Terms" = "By gifting Telegram Premium, you agree to the Telegram [Terms of Service](terms) and [Privacy Policy](privacy).";
+"Premium.Gift.Terms" = "By gifting WinterGram Premium, you agree to the WinterGram [Terms of Service](terms) and [Privacy Policy](privacy).";
"Premium.Gift.Sent.One.Title" = "Gift Sent!";
"Premium.Gift.Sent.One.Text" = "%@ has been notified about the gift you purchased.\n\nThey now have access to additional features.";
"Premium.Gift.Sent.Multiple.Title" = "Gifts Sent!";
@@ -10742,11 +10742,11 @@ Sorry for the inconvenience.";
"Premium.Gift.Sent.Close" = "Close";
"Premium.Gift.ApplyLink" = "Apply for Free";
-"Premium.Gift.ApplyLink.AlreadyHasPremium.Title" = "You already have Telegram Premium";
+"Premium.Gift.ApplyLink.AlreadyHasPremium.Title" = "You already have WinterGram Premium";
"Premium.Gift.ApplyLink.AlreadyHasPremium.Text" = "You can activate this gift link after **%@** or [send the link]() to a friend.";
-"Premium.Gift.Link.Text" = "This link allows you or [anyone you choose]() to activate a **Telegram Premium** subscription.";
-"Premium.Gift.UsedLink.Text" = "This link was used to activate a **Telegram Premium** subscription.";
+"Premium.Gift.Link.Text" = "This link allows you or [anyone you choose]() to activate a **WinterGram Premium** subscription.";
+"Premium.Gift.UsedLink.Text" = "This link was used to activate a **WinterGram Premium** subscription.";
"Premium.WhatsIncluded" = "WHAT'S INCLUDED";
@@ -10819,9 +10819,9 @@ Sorry for the inconvenience.";
"Wallpaper.NoWallpaper" = "No\nWallpaper";
"ChatList.PremiumXmasGiftTitle" = "Send gifts to **your friends**! 🎄";
-"ChatList.PremiumXmasGiftText" = "Gift Telegram Premium for Christmas.";
+"ChatList.PremiumXmasGiftText" = "Gift WinterGram Premium for Christmas.";
-"ReassignBoost.DescriptionWithLink" = "To boost **%1$@**, reassign a previous boost or [gift Telegram Premium]() to a friend to get **%2$@** additional boosts.";
+"ReassignBoost.DescriptionWithLink" = "To boost **%1$@**, reassign a previous boost or [gift WinterGram Premium]() to a friend to get **%2$@** additional boosts.";
"Channel.AdminLog.ChannelChangedNameColorAndIcon" = "%1$@ changed the channel name color and icon to %2$@ %3$@";
"Channel.AdminLog.ChannelChangedNameColor" = "%1$@ changed the channel name color to %2$@";
@@ -10850,7 +10850,7 @@ Sorry for the inconvenience.";
"Channel.Appearance.ExampleReplyText" = "Reply to your channel";
"Channel.Appearance.ExampleText" = "The color you select will be used for the channel's name";
-"Channel.Appearance.ExampleLinkWebsite" = "Telegram";
+"Channel.Appearance.ExampleLinkWebsite" = "WinterGram";
"Channel.Appearance.ExampleLinkTitle" = "Link Preview";
"Channel.Appearance.ExampleLinkText" = "This preview will also be tinted.";
@@ -10870,7 +10870,7 @@ Sorry for the inconvenience.";
"Channel.Appearance.ApplyButton" = "Apply Changes";
-"ChatList.PremiumGiftInSettingsInfo" = "You can gift **Telegram Premium** to a friend later in **Settings**.";
+"ChatList.PremiumGiftInSettingsInfo" = "You can gift **WinterGram Premium** to a friend later in **Settings**.";
"Channel.Appearance.BoostLevel" = "Level %@";
@@ -10909,7 +10909,7 @@ Sorry for the inconvenience.";
"Chat.ConfirmationDeleteFromSavedMessages" = "Delete from Saved Messages";
"Chat.ConfirmationRemoveFromSavedMessages" = "Remove from Saved Messages";
-"Premium.MaxSavedPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
+"Premium.MaxSavedPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **WinterGram Premium** to double the limit to **%2$@** chats.";
"Premium.MaxSavedPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned.";
"Premium.MaxSavedPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned.";
@@ -10955,9 +10955,9 @@ Sorry for the inconvenience.";
"Privacy.Messages.ValueEveryone" = "Everybody";
"Privacy.Messages.ValueContactsAndPremium" = "My Contacts and Premium Users";
"Privacy.Messages.SectionFooter" = "You can restrict incoming messages to only contacts and Premium users.";
-"Privacy.Messages.PremiumInfoFooter" = "[What is Telegram Premium?]()";
+"Privacy.Messages.PremiumInfoFooter" = "[What is WinterGram Premium?]()";
"Privacy.Messages.PremiumToast.Title" = "Premium Required";
-"Privacy.Messages.PremiumToast.Text" = "Subscribe to **Telegram Premium** to select this option";
+"Privacy.Messages.PremiumToast.Text" = "Subscribe to **WinterGram Premium** to select this option";
"Privacy.Messages.PremiumToast.Action" = "Add";
"Settings.Privacy.Messages" = "Messages";
@@ -10970,8 +10970,8 @@ Sorry for the inconvenience.";
"Settings.Privacy.LastSeenRevealedToast" = "Your last seen time is now visible.";
"Settings.Privacy.MessageReadTimeRevealedToast" = "Your read times are now visible.";
-"Settings.Privacy.ReadTimePremium" = "Subscribe to Telegram Premium";
-"Settings.Privacy.ReadTimePremiumFooter" = "It you subscribe to Telegram Premium, you will still see other users' last seen and read time even if you hid yours from them (unless they specifically restricted it).";
+"Settings.Privacy.ReadTimePremium" = "Subscribe to WinterGram Premium";
+"Settings.Privacy.ReadTimePremiumFooter" = "It you subscribe to WinterGram Premium, you will still see other users' last seen and read time even if you hid yours from them (unless they specifically restricted it).";
"Chat.ToastMessagingRestrictedToPremium.Text" = "**%@** only accepts messages from contacts and **Premium** users.";
"Chat.ToastMessagingRestrictedToPremium.Action" = "View";
@@ -10998,7 +10998,7 @@ Sorry for the inconvenience.";
"Chat.SearchTagsPlaceholder" = "Search messages or tags";
"PrivacyInfo.UpgradeToPremium.Title" = "Upgrade to Premium";
-"PrivacyInfo.UpgradeToPremium.ButtonTitle" = "Subscribe to Telegram Premium";
+"PrivacyInfo.UpgradeToPremium.ButtonTitle" = "Subscribe to WinterGram Premium";
"PrivacyInfo.ShowLastSeen.Title" = "Show Your Last Seen";
"PrivacyInfo.ShowLastSeen.Text" = "To see **%@'s** Last Seen time, either start showing your own Last Seen Time...";
@@ -11019,10 +11019,10 @@ Sorry for the inconvenience.";
"Chat.PrivateMessageSeenTimestamp.TodayAt" = "read today at %@";
"Chat.PrivateMessageSeenTimestamp.YesterdayAt" = "read yesterday at %@";
-"Settings.Privacy.ReadTimePremiumActive" = "Subcribed to Telegram Premium";
-"Settings.Privacy.ReadTimePremiumActiveFooter" = "Because you are a Telegram Premium subscriber, you will see the last seen and read time of all users who are sharing it with you – even if you are hiding yours.";
+"Settings.Privacy.ReadTimePremiumActive" = "Subcribed to WinterGram Premium";
+"Settings.Privacy.ReadTimePremiumActiveFooter" = "Because you are a WinterGram Premium subscriber, you will see the last seen and read time of all users who are sharing it with you – even if you are hiding yours.";
-"Privacy.VoiceMessages.NonPremiumHelp" = "Subscribe to Telegram Premium to restrict who can send you voice or video messages.\n\n[What is Telegram Premium?]()";
+"Privacy.VoiceMessages.NonPremiumHelp" = "Subscribe to WinterGram Premium to restrict who can send you voice or video messages.\n\n[What is WinterGram Premium?]()";
"ChatList.DeleteSavedPeerMyNotesConfirmation" = "Are you sure you want to delete all messages from %@?";
"ChatList.DeleteSavedPeerMyNotesConfirmationTitle" = "My Notes";
@@ -11051,7 +11051,7 @@ Sorry for the inconvenience.";
"Conversation.ForwardOptions.SenderNamesRemoved" = "Sender names removed";
-"Login.Announce.Info" = "Notify people on Telegram who know my phone number that I signed up.";
+"Login.Announce.Info" = "Notify people on WinterGram who know my phone number that I signed up.";
"Login.Announce.Notify" = "Notify";
"Login.Announce.DontNotify" = "Do Not Notify";
@@ -11069,7 +11069,7 @@ Sorry for the inconvenience.";
"BoostGift.Group.OnlyNewMembers" = "Only new members";
"BoostGift.Group.LimitMembersInfo" = "Choose if you want to limit the giveaway only to those who joined the group after the giveaway started.";
-"BoostGift.Group.DateInfo" = "Choose when %1$@ of your group will be randomly selected to receive Telegram Premium.";
+"BoostGift.Group.DateInfo" = "Choose when %1$@ of your group will be randomly selected to receive WinterGram Premium.";
"BoostGift.Group.DateInfoMembers_1" = "%@ member";
"BoostGift.Group.DateInfoMembers_any" = "%@ members";
@@ -11091,7 +11091,7 @@ Sorry for the inconvenience.";
"ChannelBoost.Table.Group.Wallpaper_any" = "%@ Group Backgrounds";
"ChannelBoost.Table.Group.CustomWallpaper" = "Custom Group Background";
-"Premium.Group.BoostByGiftDescription" = "Boost your group by gifting your members Telegram Premium. [Get boosts >]()";
+"Premium.Group.BoostByGiftDescription" = "Boost your group by gifting your members WinterGram Premium. [Get boosts >]()";
"Conversation.BoostGroup" = "BOOST";
@@ -11100,7 +11100,7 @@ Sorry for the inconvenience.";
"Premium.MessageTags" = "Tags for Messages";
"Premium.MessageTagsInfo" = "Organize your Saved Messages with tags for quicker access.";
-"Premium.MessageTags.Proceed" = "About Telegram Premium";
+"Premium.MessageTags.Proceed" = "About WinterGram Premium";
"Chat.SavedMessagesTabInfoText" = "Messages you send to [Saved Messages]()";
@@ -11156,7 +11156,7 @@ Sorry for the inconvenience.";
"Conversation.SendMessageErrorNonPremiumForbidden" = "Only Premium users can message %@";
-"Notification.GiveawayStartedGroup" = "%1$@ just started a giveaway of Telegram Premium subscriptions for its members.";
+"Notification.GiveawayStartedGroup" = "%1$@ just started a giveaway of WinterGram Premium subscriptions for its members.";
"Chat.Giveaway.Info.Group.RandomMembers_1" = "**%@** random member";
"Chat.Giveaway.Info.Group.RandomMembers_any" = "**%@** random members";
@@ -11166,11 +11166,11 @@ Sorry for the inconvenience.";
"Premium.LastSeen" = "Last Seen Times";
"Premium.LastSeenInfo" = "View the last seen and read times of others even if you hide yours.";
-"Premium.LastSeen.Proceed" = "About Telegram Premium";
+"Premium.LastSeen.Proceed" = "About WinterGram Premium";
"Premium.MessagePrivacy" = "Message Privacy";
"Premium.MessagePrivacyInfo" = "Restrict people you don't know from sending you messages.";
-"Premium.MessagePrivacy.Proceed" = "About Telegram Premium";
+"Premium.MessagePrivacy.Proceed" = "About WinterGram Premium";
"BoostGift.GroupsAndChannelsTitle" = "INCLUDED GROUPS AND CHANNELS";
"BoostGift.GroupsAndChannelsInfo" = "Choose additional groups or channels users need to join to take part in the giveaway.";
@@ -11239,7 +11239,7 @@ Sorry for the inconvenience.";
"BoostGift.GroupsOrChannels.Save" = "Save Groups and Channels";
"Group.Emoji.Title" = "Group Emoji Pack";
-"Group.Emoji.Info" = "All members will be able to use these emoji in the group, even if they don't have Telegram Premium.";
+"Group.Emoji.Info" = "All members will be able to use these emoji in the group, even if they don't have WinterGram Premium.";
"Group.Emoji.YourEmoji" = "CHOOSE EMOJI PACK";
"Group.Emoji.Placeholder" = "emojiset";
@@ -11291,7 +11291,7 @@ Sorry for the inconvenience.";
"Message.GiveawayStartedGroup" = "Group started a giveaway";
"GroupBoost.Error.BoostTooOftenText" = "You can change the group you boost only once a day. Next time you can boost is in **%@**.";
-"GroupBoost.Error.PremiumNeededText" = "Only **Telegram Premium** subscribers can boost groups. Do you want to subscribe to **Telegram Premium**?";
+"GroupBoost.Error.PremiumNeededText" = "Only **WinterGram Premium** subscribers can boost groups. Do you want to subscribe to **WinterGram Premium**?";
"Notification.GroupChangedWallpaper" = "Group set a new wallpaper";
"Notification.YouChangedGroupWallpaper" = "You set a new wallpaper for this group";
@@ -11376,16 +11376,16 @@ Sorry for the inconvenience.";
"ChannelBoost.Header.Features" = "features";
"Premium.FolderTags" = "Folder Tags";
-"Premium.FolderTagsInfo" = "Add colorful labels to chats for faster access with Telegram Premium.";
-"Premium.FolderTagsStandaloneInfo" = "Add colorful labels to chats for faster access with Telegram Premium.";
-"Premium.FolderTags.Proceed" = "About Telegram Premium";
+"Premium.FolderTagsInfo" = "Add colorful labels to chats for faster access with WinterGram Premium.";
+"Premium.FolderTagsStandaloneInfo" = "Add colorful labels to chats for faster access with WinterGram Premium.";
+"Premium.FolderTags.Proceed" = "About WinterGram Premium";
-"Premium.Business" = "Telegram Business";
+"Premium.Business" = "WinterGram Business";
"Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies.";
"Premium.Business.Location.Title" = "Location";
"Premium.Business.Location.Text" = "Display the location of your business on your account.";
-
+
"Premium.Business.Hours.Title" = "Opening Hours";
"Premium.Business.Hours.Text" = "Show to your customers when you are open for business.";
@@ -11407,7 +11407,7 @@ Sorry for the inconvenience.";
"Premium.Business.Chatbots.Title" = "Chatbots";
"Premium.Business.Chatbots.Text" = "Add any third party chatbots that will process customer interactions.";
-"Business.Title" = "Telegram Business";
+"Business.Title" = "WinterGram Business";
"Business.Description" = "Turn your account into a **business page** with these additional features.";
"Business.SubscribedDescription" = "You have now unlocked these additional business features.";
@@ -11450,7 +11450,7 @@ Sorry for the inconvenience.";
"Conversation.BoostToUnrestrictText" = "Boost this group to send messages";
-"Settings.Business" = "Telegram Business";
+"Settings.Business" = "WinterGram Business";
"Story.Editor.DiscardText" = "If you go back now, you will lose any changes you made.";
@@ -11469,7 +11469,7 @@ Sorry for the inconvenience.";
"ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED";
"ChatListFilter.TagSectionTitle" = "FOLDER COLOR";
"ChatListFilter.TagSectionFooter" = "This color will be used for the folder's tag in the chat list";
-"ChatListFilter.TagPremiumRequiredTooltipText" = "Subscribe to **Telegram Premium** to select folder color.";
+"ChatListFilter.TagPremiumRequiredTooltipText" = "Subscribe to **WinterGram Premium** to select folder color.";
"ChatListFilterList.ShowTags" = "Show Folder Tags";
"ChatListFilterList.ShowTagsFooter" = "Display folder names for each chat in the chat list.";
@@ -11503,8 +11503,8 @@ Sorry for the inconvenience.";
"Chat.Placeholder.GreetingMessage" = "Add greeting message...";
"Chat.Placeholder.AwayMessage" = "Add away message...";
-"Chat.QuickReplyMessageLimitReachedText_1" = "Limit of %d message reached";
-"Chat.QuickReplyMessageLimitReachedText_any" = "Limit of %d messages reached";
+"Chat.QuickReplyMessageLimitReachedText_1" = "Limit of %d message reached";
+"Chat.QuickReplyMessageLimitReachedText_any" = "Limit of %d messages reached";
"Chat.EmptyState.QuickReply.Title" = "New Quick Reply";
"Chat.EmptyState.QuickReply.Text1" = "· Enter a message below that will be sent in chats when you type \"**/%@\"**.";
@@ -11637,7 +11637,7 @@ Sorry for the inconvenience.";
"ChatbotSetup.TextLink" = "https://telegram.org";
"ChatbotSetup.BotSearchPlaceholder" = "Bot Username";
-"ChatbotSetup.BotSectionFooter" = "Enter the username or URL of the Telegram bot that you want to automatically process your chats.";
+"ChatbotSetup.BotSectionFooter" = "Enter the username or URL of the WinterGram bot that you want to automatically process your chats.";
"ChatbotSetup.RecipientsSectionHeader" = "CHATS ACCESSIBLE FOR THE BOT";
@@ -11698,17 +11698,17 @@ Sorry for the inconvenience.";
"Birthday.FloodError" = "Sorry, you can’t change your birthday so often.";
-"Chat.BirthdayTooltip" = "🎂 %1$@ is having a birthday today. You can give %2$@ **Telegram Premium** as a birthday gift.";
+"Chat.BirthdayTooltip" = "🎂 %1$@ is having a birthday today. You can give %2$@ **WinterGram Premium** as a birthday gift.";
"ChatList.AddBirthdayTitle" = "Add your birthday! 🎂";
"ChatList.AddBirthdayText" = "Let your contacts know when you're celebrating.";
"ChatList.BirthdaySingleTitle" = "It's %@'s **birthday** today! 🎂";
-"ChatList.BirthdaySingleText" = "Gift them Telegram Premium.";
+"ChatList.BirthdaySingleText" = "Gift them WinterGram Premium.";
"ChatList.BirthdayMultipleTitle_1" = "%@ contact has a **birthday** today! 🎂";
"ChatList.BirthdayMultipleTitle_any" = "%@ contacts have **birthdays** today! 🎂";
-"ChatList.BirthdayMultipleText" = "Gift them Telegram Premium.";
+"ChatList.BirthdayMultipleText" = "Gift them WinterGram Premium.";
"ChatList.BirthdayInSettingsInfo" = "You can set your date of birth later in **Settings**.";
@@ -11728,7 +11728,7 @@ Sorry for the inconvenience.";
"Stats.Monetization" = "Monetization";
-"Monetization.Header" = "Telegram shares 50% of the revenue from ads displayed in your channel. [Learn More >]()";
+"Monetization.Header" = "WinterGram shares 50% of the revenue from ads displayed in your channel. [Learn More >]()";
"Monetization.ImpressionsTitle" = "AD IMPRESSIONS (BY HOURS)";
"Monetization.AdRevenueTitle" = "AD REVENUE";
"Monetization.OverviewTitle" = "PROCEEDS OVERVIEW";
@@ -11775,9 +11775,9 @@ Sorry for the inconvenience.";
"Monetization.Intro.Title" = "Earn From Your Channel";
"Monetization.Intro.Bot.Title" = "Earn From Your Bot";
-"Monetization.Intro.Ads.Title" = "Telegram Ads";
-"Monetization.Intro.Ads.Text" = "Telegram can display ads in your channel.";
-"Monetization.Intro.Bot.Ads.Text" = "Telegram can display ads in your bot.";
+"Monetization.Intro.Ads.Title" = "WinterGram Ads";
+"Monetization.Intro.Ads.Text" = "WinterGram can display ads in your channel.";
+"Monetization.Intro.Bot.Ads.Text" = "WinterGram can display ads in your bot.";
"Monetization.Intro.Split.Title" = "50:50 Revenue Split";
"Monetization.Intro.Split.Text" = "You receive 50% of the ad revenue in TON.";
@@ -11786,33 +11786,33 @@ Sorry for the inconvenience.";
"Monetization.Intro.Withdrawal.Text" = "You can withdraw your TON any time.";
"Monetization.Intro.Info.Title" = "What's #TON?";
-"Monetization.Intro.Info.Text" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and commissions on transactions. [Learn More >]()";
+"Monetization.Intro.Info.Text" = "TON is a blockchain platform and cryptocurrency that WinterGram uses for its high speed and commissions on transactions. [Learn More >]()";
"Monetization.Intro.Info.Text_URL" = "https://ton.org";
"Monetization.Intro.Understood" = "Understood";
"ReportAd.Title" = "Report Ad";
-"ReportAd.Help" = "Learn more about [Telegram Ad Policies and Guidelines]().";
+"ReportAd.Help" = "Learn more about [WinterGram Ad Policies and Guidelines]().";
"ReportAd.Help_URL" = "https://ads.telegram.org/guidelines";
"ReportAd.Reported" = "We will review this ad to ensure it matches our [Ad Policies and Guidelines]().";
-"ReportAd.Hidden" = "You will no longer see ads from Telegram.";
+"ReportAd.Hidden" = "You will no longer see ads from WinterGram.";
"AdsInfo.Title" = "About These Ads";
-"AdsInfo.Info" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:";
+"AdsInfo.Info" = "WinterGram Ads are very different from ads on other platforms. Ads such as this one:";
"AdsInfo.Respect.Title" = "Respect Your Privacy";
-"AdsInfo.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them.";
-"AdsInfo.Bot.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the bot in which you see them.";
+"AdsInfo.Respect.Text" = "Ads on WinterGram do not use your personal information and are based on the channel in which you see them.";
+"AdsInfo.Bot.Respect.Text" = "Ads on WinterGram do not use your personal information and are based on the bot in which you see them.";
"AdsInfo.Split.Title" = "Help the Channel Creator";
-"AdsInfo.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed.";
+"AdsInfo.Split.Text" = "50% of the revenue from WinterGram Ads goes to the owner of the channel where they are displayed.";
"AdsInfo.Bot.Split.Title" = "Help the Bot Creator";
-"AdsInfo.Bot.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the bot where they are displayed.";
+"AdsInfo.Bot.Split.Text" = "50% of the revenue from WinterGram Ads goes to the owner of the bot where they are displayed.";
"AdsInfo.Ads.Title" = "Can Be Removed";
-"AdsInfo.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium](), and Level %@ channels can remove them for their subscribers.";
-"AdsInfo.Bot.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium]().";
+"AdsInfo.Ads.Text" = "You can turn off ads by subscribing to [WinterGram Premium](), and Level %@ channels can remove them for their subscribers.";
+"AdsInfo.Bot.Ads.Text" = "You can turn off ads by subscribing to [WinterGram Premium]().";
"AdsInfo.Launch.Title" = "Can I Launch an Ad?";
-"AdsInfo.Launch.Text" = "Anyone can create ads to display in this channel – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()";
-"AdsInfo.Bot.Launch.Text" = "Anyone can create ads to display in this bot – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()";
+"AdsInfo.Launch.Text" = "Anyone can create ads to display in this channel – with minimal budgets. Check out the WinterGram Ad Platform for details. [Learn More >]()";
+"AdsInfo.Bot.Launch.Text" = "Anyone can create ads to display in this bot – with minimal budgets. Check out the WinterGram Ad Platform for details. [Learn More >]()";
"AdsInfo.Launch.Text_URL" = "https://promote.telegram.org";
"AdsInfo.Understood" = "Understood";
@@ -11865,7 +11865,7 @@ Sorry for the inconvenience.";
"SendInviteLink.ChannelTextContactsAndPremiumMultipleUsers_1" = "{user_list}, and **%d** more person only accept invitations to channels from contacts and **Premium** users.";
"SendInviteLink.ChannelTextContactsAndPremiumMultipleUsers_any" = "{user_list}, and **%d** more people only accept invitations to channels from contacts and **Premium** users.";
-"SendInviteLink.SubscribeToPremiumButton" = "Subscribe to Telegram Premium";
+"SendInviteLink.SubscribeToPremiumButton" = "Subscribe to WinterGram Premium";
"SendInviteLink.PremiumOrSendSectionSeparator" = "or";
"SendInviteLink.TextSendInviteLink" = "You can try to send an invite link instead.";
@@ -11913,7 +11913,7 @@ Sorry for the inconvenience.";
"ChatbotSetup.Recipients.ExcludedListTitle" = "Excluded Chats";
"ChatbotSetup.Recipients.IncludedListTitle" = "Included Chats";
-"ChatbotSetup.ErrorBotNotBusinessCapable" = "This bot doesn't support Telegram Business yet.";
+"ChatbotSetup.ErrorBotNotBusinessCapable" = "This bot doesn't support WinterGram Business yet.";
"ChatbotSetup.RecipientSummary.ValueEmpty" = "Add";
"ChatbotSetup.RecipientSummary.ValueItems_1" = "1 item";
@@ -11925,10 +11925,10 @@ Sorry for the inconvenience.";
"Chat.ToastPhoneNumberCopied" = "Phone number copied to clipboard.";
"Chat.SpeedLimitAlert.Download.Title" = "Download speed limited";
-"Chat.SpeedLimitAlert.Download.Text" = "Subscribe to [Telegram Premium]() and increase download speeds %@ times.";
+"Chat.SpeedLimitAlert.Download.Text" = "Subscribe to [WinterGram Premium]() and increase download speeds %@ times.";
"Chat.SpeedLimitAlert.Upload.Title" = "Upload speed limited";
-"Chat.SpeedLimitAlert.Upload.Text" = "Subscribe to [Telegram Premium]() and increase upload speeds %@ times.";
+"Chat.SpeedLimitAlert.Upload.Text" = "Subscribe to [WinterGram Premium]() and increase upload speeds %@ times.";
"Chat.BusinessBotMessageTooltip" = "Only you can see that this\nmessage was sent by the bot.";
@@ -11952,10 +11952,10 @@ Sorry for the inconvenience.";
"Business.AdsTitle" = "ADS IN CHANNELS";
"Business.DontHideAds" = "Do Not Hide Ads";
-"Business.AdsInfo" = "As a Premium subscriber, you don't see any ads on Telegram, but you can turn them on, for example, to view your own ads that you launched on the [Telegram Ad Platform >]()";
+"Business.AdsInfo" = "As a Premium subscriber, you don't see any ads on WinterGram, but you can turn them on, for example, to view your own ads that you launched on the [WinterGram Ad Platform >]()";
"Business.AdsInfo_URL" = "https://promote.telegram.org";
-"ChatList.PremiumGraceTitle" = "⚠️ Don't lose access to Telegram Premium!";
+"ChatList.PremiumGraceTitle" = "⚠️ Don't lose access to WinterGram Premium!";
"ChatList.PremiumGraceText" = "Your exclusive benefits are about to expire.";
"Stickers.RemoveFromRecent" = "Remove from Recents";
@@ -12256,19 +12256,19 @@ Sorry for the inconvenience.";
"Chat.Context.Phone.CreateNewContact" = "Create New Contact";
"Chat.Context.Phone.AddToExistingContact" = "Add to Existing Contact";
"Chat.Context.Phone.SendMessage" = "Send Message";
-"Chat.Context.Phone.TelegramVoiceCall" = "Telegram Voice Call";
-"Chat.Context.Phone.TelegramVideoCall" = "Telegram Video Call";
-"Chat.Context.Phone.InviteToTelegram" = "Invite to Telegram";
+"Chat.Context.Phone.TelegramVoiceCall" = "WinterGram Voice Call";
+"Chat.Context.Phone.TelegramVideoCall" = "WinterGram Video Call";
+"Chat.Context.Phone.InviteToTelegram" = "Invite to WinterGram";
"Chat.Context.Phone.CallViaCarrier" = "Call via Carrier";
"Chat.Context.Phone.CopyNumber" = "Copy Number";
-"Chat.Context.Phone.NotOnTelegram" = "This number is not on Telegram.";
+"Chat.Context.Phone.NotOnTelegram" = "This number is not on WinterGram.";
"Chat.Context.Phone.ViewProfile" = "View Profile";
"Chat.Context.Username.SendMessage" = "Send Message";
"Chat.Context.Username.OpenGroup" = "Open Group";
"Chat.Context.Username.OpenChannel" = "Open Channel";
"Chat.Context.Username.Copy" = "Copy Username";
-"Chat.Context.Username.NotOnTelegram" = "This user doesn't exist on Telegram.";
+"Chat.Context.Username.NotOnTelegram" = "This user doesn't exist on WinterGram.";
"Chat.Context.Hashtag.Search" = "Search";
"Chat.Context.Hashtag.Copy" = "Copy Hashtag";
@@ -12292,8 +12292,8 @@ Sorry for the inconvenience.";
"Conversation.ContextMenuAddFactCheck" = "Add Fact Check";
"Conversation.ContextMenuEditFactCheck" = "Edit Fact Check";
-"Stars.Intro.Title" = "Telegram Stars";
-"Stars.Intro.Description" = "Buy Stars to unlock content and services in miniapps on Telegram.";
+"Stars.Intro.Title" = "WinterGram Stars";
+"Stars.Intro.Description" = "Buy Stars to unlock content and services in miniapps on WinterGram.";
"Stars.Intro.Balance" = "Balance";
"Stars.Intro.YourBalance" = "your balance";
"Stars.Intro.Buy" = "Buy More Stars";
@@ -12314,7 +12314,7 @@ Sorry for the inconvenience.";
"Stars.Intro.Transaction.FragmentWithdrawal.Title" = "Withdrawal";
"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment";
"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal";
-"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads";
+"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via WinterGram Ads";
"Stars.Intro.Transaction.Gift" = "Gift";
"Stars.Intro.Transaction.ConvertedGift" = "Converted Gift";
"Stars.Intro.Transaction.Unsupported.Title" = "Unsupported";
@@ -12364,7 +12364,7 @@ Sorry for the inconvenience.";
"Stars.Transaction.FragmentWithdrawal.Title" = "Stars Withdrawal";
"Stars.Transaction.FragmentWithdrawal.Subtitle" = "Fragment";
"Stars.Transaction.TelegramAds.Title" = "Stars Withdrawal";
-"Stars.Transaction.TelegramAds.Subtitle" = "Telegram Ads";
+"Stars.Transaction.TelegramAds.Subtitle" = "WinterGram Ads";
"Stars.Transaction.Unsupported.Title" = "Unsupported";
"Stars.Transaction.Refund" = "Refund";
@@ -12407,7 +12407,7 @@ Sorry for the inconvenience.";
"Chat.MessageEffectMenu.SectionMessageEffects" = "Message Effects";
"Chat.SendMessageMenu.MoveCaptionUp" = "Move Caption Up";
"Chat.SendMessageMenu.MoveCaptionDown" = "Move Caption Down";
-"Chat.SendMessageMenu.ToastPremiumRequired.Text" = "Subscribe to [Telegram Premium]() to add this animated effect.";
+"Chat.SendMessageMenu.ToastPremiumRequired.Text" = "Subscribe to [WinterGram Premium]() to add this animated effect.";
"BusinessLink.AlertTextLimitText" = "The message text limit is 4096 characters";
@@ -12478,7 +12478,7 @@ Sorry for the inconvenience.";
"MediaEditor.Link.LinkName.Title" = "LINK NAME (OPTIONAL)";
"MediaEditor.Link.LinkName.Placeholder" = "Enter a Name";
-"Story.Editor.TooltipLinkPremium" = "Subscribe to [Telegram Premium]() to add links.";
+"Story.Editor.TooltipLinkPremium" = "Subscribe to [WinterGram Premium]() to add links.";
"Story.Editor.TooltipLinkLimitValue_1" = "**%@** link";
"Story.Editor.TooltipLinkLimitValue_any" = "**%@** links";
@@ -12547,7 +12547,7 @@ Sorry for the inconvenience.";
"WebApp.Share" = "Share";
"Stars.Purchase.GiftStars" = "Gift Stars";
-"Stars.Purchase.GiftInfo" = "With Stars, **%1$@** will be able to unlock content and services on Telegram. [See Examples >]()";
+"Stars.Purchase.GiftInfo" = "With Stars, **%1$@** will be able to unlock content and services on WinterGram. [See Examples >]()";
"Stars.Purchase.SubscriptionInfo" = "Buy Stars to subscribe for **%@**.";
"Stars.Purchase.SubscriptionRenewInfo" = "Buy Stars to keep your subscription for **%@**.";
@@ -12568,8 +12568,8 @@ Sorry for the inconvenience.";
"Notification.StarsGift.Title_1" = "%@ Star";
"Notification.StarsGift.Title_any" = "%@ Stars";
-"Notification.StarsGift.Subtitle" = "Use Stars to unlock content and services on Telegram.";
-"Notification.StarsGift.SubtitleYou" = "With Stars, %@ will be able to unlock content and services on Telegram.";
+"Notification.StarsGift.Subtitle" = "Use Stars to unlock content and services on WinterGram.";
+"Notification.StarsGift.SubtitleYou" = "With Stars, %@ will be able to unlock content and services on WinterGram.";
"Notification.StarsGift.UnknownUser" = "Unknown user";
"Bot.Settings" = "Bot Settings";
@@ -12582,7 +12582,7 @@ Sorry for the inconvenience.";
"Browser.ContextMenu.CopyLink" = "Copy Link";
"Browser.ContextMenu.Share" = "Share";
-"WebBrowser.Telegram" = "Telegram";
+"WebBrowser.Telegram" = "WinterGram";
"Monetization.Proceeds.Ton.Info" = "TON from your total balance can be used for ads or withdrawn as rewards 3 days after they are earned.";
"Monetization.Proceeds.Stars.Info" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.";
@@ -12609,10 +12609,10 @@ Sorry for the inconvenience.";
"Stars.Intro.StarsSent.ViewChat" = "View Chat";
"Stars.Gift.Received.Title" = "Received Gift";
-"Stars.Gift.Received.Text" = "Use Stars to unlock content and services on Telegram. [See Examples >]()";
+"Stars.Gift.Received.Text" = "Use Stars to unlock content and services on WinterGram. [See Examples >]()";
"Stars.Gift.Sent.Title" = "Sent Gift";
-"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()";
+"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on WinterGram. [See Examples >]()";
"WebBrowser.Reload" = "Reload";
"WebBrowser.Share" = "Share";
@@ -12627,11 +12627,11 @@ Sorry for the inconvenience.";
"WebBrowser.AddressBar.ShowMore" = "Show More";
"WebBrowser.OpenLinksIn.Title" = "OPEN LINKS IN";
-"WebBrowser.AutoLogin" = "Auto-Login via Telegram";
-"WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser.";
+"WebBrowser.AutoLogin" = "Auto-Login via WinterGram";
+"WebBrowser.AutoLogin.Info" = "Use your WinterGram account to automatically log in to websites opened in the in-app browser.";
"WebBrowser.ClearCookies" = "Clear Cookies";
-"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites.";
+"WebBrowser.ClearCookies.Info" = "Delete all cookies in the WinterGram in-app browser. This action will sign you out of most websites.";
"WebBrowser.ClearCookies.Succeed" = "Cookies cleared.";
"WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER";
@@ -12657,7 +12657,7 @@ Sorry for the inconvenience.";
"WebBrowser.Done" = "Done";
-"AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON.";
+"AccessDenied.LocationWeather" = "WinterGram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set WinterGram to ON.";
"Story.Editor.TooltipWeatherLimitText" = "You can't add more than one weather sticker to a story.";
@@ -12683,7 +12683,7 @@ Sorry for the inconvenience.";
"PeerInfo.PaneBotPreviews" = "Preview";
"PeerInfo.OpenAppButton" = "Open App";
-"PeerInfo.AppFooterAdmin" = "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy).";
+"PeerInfo.AppFooterAdmin" = "By publishing this mini app, you agree to the [WinterGram Terms of Service for Developers](https://telegram.org/privacy).";
"PeerInfo.AppFooter" = "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy).";
"BotPreviews.MenuAddPreview" = "Add Preview";
"BotPreviews.MenuReorder" = "Reorder";
@@ -12787,7 +12787,7 @@ Sorry for the inconvenience.";
"Stars.Intro.Transaction.SubscriptionFee.Title" = "Monthly Subscription Fee";
"Stars.Intro.Transaction.Reaction.Title" = "Star Reaction";
-"Stars.Purchase.GenericPurchasePurpose" = "Buy Stars to unlock content and services on Telegram.";
+"Stars.Purchase.GenericPurchasePurpose" = "Buy Stars to unlock content and services on WinterGram.";
"Stars.Purchase.PurchasePurpose.subs" = "Buy Stars to keep all your subscriptions.";
"Stars.Transfer.Subscribe.Channel.Title" = "Subscribe";
@@ -12858,7 +12858,7 @@ Sorry for the inconvenience.";
"SendStarReactions.TermsOfServiceFooter" = "By sending Stars you agree to the [Terms of Service](https://telegram.org/tos/stars)";
"PeerInfo.AllowedReactions.StarReactions" = "Enable Paid Reactions";
-"PeerInfo.AllowedReactions.StarReactionsFooter" = "Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)";
+"PeerInfo.AllowedReactions.StarReactionsFooter" = "Switch this on to let your subscribers set paid reactions with WinterGram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)";
"Chat.ToastStarsSent.Title_1" = "Star sent!";
"Chat.ToastStarsSent.Title_any" = "Stars sent!";
@@ -12881,13 +12881,13 @@ Sorry for the inconvenience.";
"Notification.StarsPrize" = "You received a gift";
-"Notification.GiveawayStartedStars" = "%1$@ just started a giveaway of %2$@ Telegram Stars for its followers.";
-"Notification.GiveawayStartedStarsGroup" = "%1$@ just started a giveaway of %2$@ Telegram Stars for its members.";
+"Notification.GiveawayStartedStars" = "%1$@ just started a giveaway of %2$@ WinterGram Stars for its followers.";
+"Notification.GiveawayStartedStarsGroup" = "%1$@ just started a giveaway of %2$@ WinterGram Stars for its members.";
"Notification.StarsGiveawayStarted" = "%1$@ just started a giveaway of %2$@ for its followers.";
"Notification.StarsGiveawayStartedGroup" = "%1$@ just started a giveaway of %2$@ for its members.";
-"Notification.StarsGiveawayStarted.Stars_1" = "%@ Telegram Star";
-"Notification.StarsGiveawayStarted.Stars_any" = "%@ Telegram Stars";
+"Notification.StarsGiveawayStarted.Stars_1" = "%@ WinterGram Star";
+"Notification.StarsGiveawayStarted.Stars_any" = "%@ WinterGram Stars";
"Chat.Giveaway.Message.Stars.PrizeText" = "%1$@ will be distributed %2$@.";
"Chat.Giveaway.Message.Stars.Stars_1" = "**%@** Stars";
@@ -12916,8 +12916,8 @@ Sorry for the inconvenience.";
"BoostGift.NewDescriptionGroup" = "Get more boosts and members for\nyour group by giving away prizes.";
"BoostGift.NewDescription" = "Get more boosts and subscribers for\nyour channel by giving away prizes.";
"BoostGift.Prize" = "PRIZE";
-"BoostGift.Prize.Premium" = "Telegram Premium";
-"BoostGift.Prize.Stars" = "Telegram Stars";
+"BoostGift.Prize.Premium" = "WinterGram Premium";
+"BoostGift.Prize.Stars" = "WinterGram Stars";
"BoostGift.Stars.Title" = "STARS TO DISTRIBUTE";
"BoostGift.Stars.Boosts_1" = "%@ BOOST";
"BoostGift.Stars.Boosts_any" = "%@ BOOSTS";
@@ -12939,8 +12939,8 @@ Sorry for the inconvenience.";
"BoostGift.Group.StarsDateInfo" = "Choose when %@ of your group will be randomly selected to receive Stars.";
"BoostGift.StarsDateInfo" = "Choose when %@ of your channel will be randomly selected to receive Stars.";
-"BoostGift.PrepaidGiveawayStarsCount_1" = "%@ Telegram Premium";
-"BoostGift.PrepaidGiveawayStarsCount_any" = "%@ Telegram Premium";
+"BoostGift.PrepaidGiveawayStarsCount_1" = "%@ WinterGram Premium";
+"BoostGift.PrepaidGiveawayStarsCount_any" = "%@ WinterGram Premium";
"BoostGift.PrepaidGiveawayStarsMonths" = "%@-month subscriptions";
"WebBrowser.ShowInstantView" = "Show Instant View";
@@ -12975,8 +12975,8 @@ Sorry for the inconvenience.";
"Premium.BoostByGiveawayDescription" = "Get more boosts and subscribers for your channel by giving away prizes. [Get boosts >]()";
"Premium.Group.BoostByGiveawayDescription" = "Get more boosts and members for your group by giving away prizes. [Get boosts >]()";
-"Notification.StarsGiveawayResultsNoWinners" = "Due to the giveaway terms, no winners could be selected by Telegram, all stars were credited to channel administrators.";
-"Notification.StarsGiveawayResultsNoWinners.Group" = "Due to the giveaway terms, no winners could be selected by Telegram, all stars were credited to group administrators.";
+"Notification.StarsGiveawayResultsNoWinners" = "Due to the giveaway terms, no winners could be selected by WinterGram, all stars were credited to channel administrators.";
+"Notification.StarsGiveawayResultsNoWinners.Group" = "Due to the giveaway terms, no winners could be selected by WinterGram, all stars were credited to group administrators.";
"Notification.StarsGiveaway.Title" = "Congratulations!";
"Notification.StarsGiveaway.Subtitle" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is **%2$@**.";
@@ -12993,13 +12993,13 @@ Sorry for the inconvenience.";
"SharedMedia.GiftCount_any" = "%@ gifts";
"Stars.Info.Title" = "What are Stars?";
-"Stars.Info.Description" = "Telegram Stars allow users to:";
+"Stars.Info.Description" = "WinterGram Stars allow users to:";
"Stars.Info.Gift.Title" = "Send Gifts to Friends";
"Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars.";
"Stars.Info.Miniapp.Title" = "Use Stars in Miniapps";
-"Stars.Info.Miniapp.Text" = "Buy additional content and services in Telegram miniapps. [See Examples >]()";
+"Stars.Info.Miniapp.Text" = "Buy additional content and services in WinterGram miniapps. [See Examples >]()";
"Stars.Info.Media.Title" = "Unlock Content in Channels";
-"Stars.Info.Media.Text" = "Get access to paid content and services in Telegram channels.";
+"Stars.Info.Media.Text" = "Get access to paid content and services in WinterGram channels.";
"Stars.Info.Reaction.Title" = "Send Star Reactions";
"Stars.Info.Reaction.Text" = "Support your favorite channels by sending Star reactions to their posts.";
"Stars.Info.Done" = "Got It";
@@ -13063,7 +13063,7 @@ Sorry for the inconvenience.";
"Gift.Convert.Success.Text.Stars_any" = "%@ Stars";
"Gift.Options.Premium.Title" = "Gift Premium";
-"Gift.Options.Premium.Text" = "Give **%@** access to exclusive features with Telegram Premium. [See Features >]()";
+"Gift.Options.Premium.Text" = "Give **%@** access to exclusive features with WinterGram Premium. [See Features >]()";
"Gift.Options.Premium.Months_1" = "%@ month";
"Gift.Options.Premium.Months_any" = "%@ months";
"Gift.Options.Premium.Years_1" = "%@ year";
@@ -13144,7 +13144,7 @@ Sorry for the inconvenience.";
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
"Notification.PremiumGift.More" = "more";
-"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";
+"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive WinterGram features.";
"Notification.StarsGift.Stars_1" = "%@ Star";
"Notification.StarsGift.Stars_any" = "%@ Stars";
@@ -13185,7 +13185,7 @@ Sorry for the inconvenience.";
"Stars.Transaction.TelegramBotApi.Messages_1" = "%@ Message";
"Stars.Transaction.TelegramBotApi.Messages_any" = "%@ Messages";
-"Monetization.Bot.Header" = "Telegram shares 50% of the revenue from ads displayed in your bot. [Learn More >]()";
+"Monetization.Bot.Header" = "WinterGram shares 50% of the revenue from ads displayed in your bot. [Learn More >]()";
"Monetization.Bot.BalanceTitle" = "AVAILABLE BALANCE";
"Resolve.ChannelErrorNotFound" = "Sorry, this channel doesn't seem to exist.";
@@ -13395,7 +13395,7 @@ Sorry for the inconvenience.";
"AffiliateSetup.IntroNew.Title1" = "Share revenue with affiliates";
"AffiliateSetup.IntroNew.Text1" = "Set the commission for revenue generated by users referred to you.";
"AffiliateSetup.IntroNew.Title2" = "Launch your affiliate program";
-"AffiliateSetup.IntroNew.Text2" = "Telegram will feature your program for millions of potential affiliates.";
+"AffiliateSetup.IntroNew.Text2" = "WinterGram will feature your program for millions of potential affiliates.";
"AffiliateSetup.IntroNew.Title3" = "Let affiliates promote you";
"AffiliateSetup.IntroNew.Text3" = "Affiliates will share your referral link with their audience.";
@@ -13510,8 +13510,8 @@ Sorry for the inconvenience.";
"Gift.Upgrade.Unique.Description" = "Get a unique number, model, backdrop, and symbol for your gift.";
"Gift.Upgrade.Unique.IncludeDescription" = "The recipient will get a unique number, model, backdrop, and symbol for the gift.";
"Gift.Upgrade.Transferable.Title" = "Transferable";
-"Gift.Upgrade.Transferable.Description" = "Send your upgraded gift to any of your friends on Telegram.";
-"Gift.Upgrade.Transferable.IncludeDescription" = "The recipient will be able to send the gift to anyone Telegram.";
+"Gift.Upgrade.Transferable.Description" = "Send your upgraded gift to any of your friends on WinterGram.";
+"Gift.Upgrade.Transferable.IncludeDescription" = "The recipient will be able to send the gift to anyone WinterGram.";
"Gift.Upgrade.Tradable.Title" = "Tradable";
"Gift.Upgrade.Tradable.Description" = "Sell or auction your gift on third-party NFT marketplaces.";
"Gift.Upgrade.Tradable.IncludeDescription" = "The recipient will be able to auction the gift on third-party NFT marketplaces.";
@@ -13547,12 +13547,12 @@ Sorry for the inconvenience.";
"Gift.Transfer.SendUnlocks.Days_any" = "%@ days";
"Gift.Transfer.UnlockPending.Title" = "Unlocking In Progress";
-"Gift.Transfer.UnlockPending.Text" = "In %@, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.";
+"Gift.Transfer.UnlockPending.Text" = "In %@, you'll be able to send this collectible to any TON blockchain address outside WinterGram for sale or auction.";
"Gift.Transfer.UnlockPending.Text.Days_1" = "%@ day";
"Gift.Transfer.UnlockPending.Text.Days_any" = "%@ days";
"Gift.Transfer.UpdateRequired.Title" = "Update Required";
-"Gift.Transfer.UpdateRequired.Text" = "Please update your Telegram application to the latest version.";
+"Gift.Transfer.UpdateRequired.Text" = "Please update your WinterGram application to the latest version.";
"Gift.Transfer.Confirmation.Title" = "Confirm Transfer";
"Gift.Transfer.Confirmation.TextFree" = "Dou you want to transfer ownership of **%1$@** to **%2$@**?";
@@ -13700,7 +13700,7 @@ Sorry for the inconvenience.";
"Gift.View.PutOn" = "You put on %@";
"Gift.View.TookOff" = "You took off %@";
-"Gift.View.TooltipPremiumWearing" = "Subscribe to [Telegram Premium]() to wear collectibles.";
+"Gift.View.TooltipPremiumWearing" = "Subscribe to [WinterGram Premium]() to wear collectibles.";
"Conversation.AddToContactsLong" = "Add to Contacts";
@@ -13717,7 +13717,7 @@ Sorry for the inconvenience.";
"SharedMedia.SimilarBot.Users_any" = "%@ monthly users";
"PeerInfo.SimilarBots.ShowMore" = "Show More Bots";
-"PeerInfo.SimilarBots.ShowMoreInfo" = "Subscribe to [Telegram Premium]()\nto unlock up to **100** similar bots.";
+"PeerInfo.SimilarBots.ShowMoreInfo" = "Subscribe to [WinterGram Premium]()\nto unlock up to **100** similar bots.";
"PeerInfo.VerifyAccounts" = "Verify Accounts";
@@ -13726,7 +13726,7 @@ Sorry for the inconvenience.";
"Gift.View.Context.Transfer" = "Transfer";
"Gift.Withdraw.Title" = "Manage with Fragment";
-"Gift.Withdraw.Text" = "You can use Fragment, a third-party service, to transfer **%@** to your TON account. After that, you can manage it as an NFT with any TON wallet outside Telegram.\n\nYou can also move such NFTs back to your Telegram account via Fragment.";
+"Gift.Withdraw.Text" = "You can use Fragment, a third-party service, to transfer **%@** to your TON account. After that, you can manage it as an NFT with any TON wallet outside WinterGram.\n\nYou can also move such NFTs back to your WinterGram account via Fragment.";
"Gift.Withdraw.Proceed" = "Open Fragment";
"ChatListFilter.NameEnableAnimations" = "Enable Animations";
@@ -13741,8 +13741,8 @@ Sorry for the inconvenience.";
"PeerInfo.Gifts.SendGift" = "Send Gift";
"PeerInfo.Gifts.ChannelNotify" = "Notify About New Gifts";
-"PeerInfo.Gifts.ChannelNotifyTooltip" = "You will receive a message from Telegram when your channel receives a gift.";
-"PeerInfo.Gifts.ChannelNotifyDisabledTooltip" = "You will not receive a message from Telegram when your channel receives a gift.";
+"PeerInfo.Gifts.ChannelNotifyTooltip" = "You will receive a message from WinterGram when your channel receives a gift.";
+"PeerInfo.Gifts.ChannelNotifyDisabledTooltip" = "You will not receive a message from WinterGram when your channel receives a gift.";
"Notification.StarsGift.Channel.Sent" = "%1$@ sent a gift to %2$@ for %3$@";
@@ -13830,9 +13830,9 @@ Sorry for the inconvenience.";
"Stars.Transfer.Terms" = "By purchasing you agree to the [Terms of Service]().";
"Stars.Transfer.Terms_URL" = "https://telegram.org/tos/stars";
-"AvatarEditor.PremiumNeeded.Background" = "Subscribe to Telegram Premium to choose this background.";
-"AvatarEditor.PremiumNeeded.Emoji" = "Subscribe to Telegram Premium to choose this emoji.";
-"AvatarEditor.PremiumNeeded.Sticker" = "Subscribe to Telegram Premium to choose this sticker.";
+"AvatarEditor.PremiumNeeded.Background" = "Subscribe to WinterGram Premium to choose this background.";
+"AvatarEditor.PremiumNeeded.Emoji" = "Subscribe to WinterGram Premium to choose this emoji.";
+"AvatarEditor.PremiumNeeded.Sticker" = "Subscribe to WinterGram Premium to choose this sticker.";
"PremiumNeeded.Title" = "Premium Needed";
"PremiumNeeded.Subscribe" = "Subscribe";
@@ -13931,11 +13931,11 @@ Sorry for the inconvenience.";
"Privacy.Messages.Stars_1" = "%@ Star";
"Privacy.Messages.Stars_any" = "%@ Stars";
-"Privacy.Messages.Unlock" = "Unlock with Telegram Premium";
+"Privacy.Messages.Unlock" = "Unlock with WinterGram Premium";
"Premium.PaidMessages" = "Paid Messages";
"Premium.PaidMessagesInfo" = "Charge a fee for messages from non-contacts or new senders.";
-"Premium.PaidMessages.Proceed" = "About Telegram Premium";
+"Premium.PaidMessages.Proceed" = "About WinterGram Premium";
"PeerInfo.Gifts.Reorder" = "Reorder";
"PeerInfo.Gifts.Context.Pin" = "Pin";
@@ -13949,7 +13949,7 @@ Sorry for the inconvenience.";
"PeerInfo.Gifts.Context.Transfer" = "Transfer";
"Gift.Send.Premium.Confirmation.Title" = "Send a Gift";
-"Gift.Send.Premium.Confirmation.Text" = "Are you sure you want to gift **Telegram Premium** to **%1$@** for **%2$@**?";
+"Gift.Send.Premium.Confirmation.Text" = "Are you sure you want to gift **WinterGram Premium** to **%1$@** for **%2$@**?";
"Gift.Send.Premium.Confirmation.Text.Stars_1" = "%@ Star";
"Gift.Send.Premium.Confirmation.Text.Stars_any" = "%@ Stars";
"Gift.Send.Premium.Confirmation.Confirm" = "Confirm";
@@ -14038,7 +14038,7 @@ Sorry for the inconvenience.";
"Premium.MaxExpiringStoriesTextNumberFormat_any" = "**%d** stories";
"Premium.MaxExpiringStoriesTextPremiumNumberFormat_1" = "**%d** story";
"Premium.MaxExpiringStoriesTextPremiumNumberFormat_any" = "**%d** stories";
-"Premium.MaxExpiringStoriesTextFormat" = "You can post %@ in **24** hours. Subscribe to **Telegram Premium** to increase this limit to **%@**.";
+"Premium.MaxExpiringStoriesTextFormat" = "You can post %@ in **24** hours. Subscribe to **WinterGram Premium** to increase this limit to **%@**.";
"Premium.MaxExpiringStoriesNoPremiumTextNumberFormat_1" = "**%d** story";
"Premium.MaxExpiringStoriesNoPremiumTextNumberFormat_any" = "**%d** stories";
@@ -14051,7 +14051,7 @@ Sorry for the inconvenience.";
"Stars.AccountRevenue.Proceeds.Info" = "Stars from your total balance can be withdrawn as rewards 21 days after they are earned.";
"Stars.AccountRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment. You cannot withdraw less than 1000 stars. [Learn More >]()";
-"Privacy.Gifts.PremiumToast.Text" = "Subscribe to **Telegram Premium** to use this setting.";
+"Privacy.Gifts.PremiumToast.Text" = "Subscribe to **WinterGram Premium** to use this setting.";
"Privacy.Gifts.PremiumToast.Action" = "Open";
"Gift.Send.ErrorDisallowed" = "**%@** doesn't accept this kind of gifts.";
@@ -14096,7 +14096,7 @@ Sorry for the inconvenience.";
"FrozenAccount.Title" = "Your Account is Frozen";
"FrozenAccount.Violation.Title" = "Violation of Terms";
-"FrozenAccount.Violation.Text" = "Your account was frozen for breaking Telegram's Terms and Conditions.";
+"FrozenAccount.Violation.Text" = "Your account was frozen for breaking WinterGram's Terms and Conditions.";
"FrozenAccount.ReadOnly.Title" = "Read-Only Mode";
"FrozenAccount.ReadOnly.Text" = "You can access your account but can't send messages or take actions.";
"FrozenAccount.Appeal.Title" = "Appeal Before Deactivation";
@@ -14105,8 +14105,8 @@ Sorry for the inconvenience.";
"FrozenAccount.Understood" = "Understood";
"AdsInfo.Search.Respect.Text" = "Ads like this do not use your personal information and are based on the search query you entered.";
-"AdsInfo.Search.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium]().";
-"AdsInfo.Search.Launch.Text" = "Anyone can create an ad to display in search results for any query. Check out the Telegram Ad Platform for details. [Learn More >]()";
+"AdsInfo.Search.Ads.Text" = "You can turn off ads by subscribing to [WinterGram Premium]().";
+"AdsInfo.Search.Launch.Text" = "Anyone can create an ad to display in search results for any query. Check out the WinterGram Ad Platform for details. [Learn More >]()";
"ChatList.FrozenAccount.Title" = "Your account is frozen";
"ChatList.FrozenAccount.Text" = "Tap to view details and submit an appeal.";
@@ -14120,18 +14120,18 @@ Sorry for the inconvenience.";
"Login.Fee.Title" = "SMS Fee";
"Login.Fee.SmsCost.Title" = "High SMS Costs";
-"Login.Fee.SmsCost.Text" = "Telecom providers in your country (%@) charge Telegram very high prices for SMS.";
+"Login.Fee.SmsCost.Text" = "Telecom providers in your country (%@) charge WinterGram very high prices for SMS.";
"Login.Fee.Verification.Title" = "Verification Required";
-"Login.Fee.Verification.Text" = "Telegram needs to send you an SMS with a verification code to confirm your phone number.";
-"Login.Fee.Support.Title" = "Support via [Telegram Premium >]()";
-"Login.Fee.Support.Text" = "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.";
+"Login.Fee.Verification.Text" = "WinterGram needs to send you an SMS with a verification code to confirm your phone number.";
+"Login.Fee.Support.Title" = "Support via [WinterGram Premium >]()";
+"Login.Fee.Support.Text" = "Sign up for a 1-week WinterGram Premium subscription to help cover the SMS costs.";
"Login.Fee.SignUp" = "Sign Up for %@";
-"Login.Fee.GetPremiumForAWeek" = "Get Telegram Premium for 1 week";
+"Login.Fee.GetPremiumForAWeek" = "Get WinterGram Premium for 1 week";
"StoryFeed.AddStory" = "Add Story";
"WebApp.ImportData.Title" = "Import Data";
-"WebApp.ImportData.Description" = "**%@** is requesting permission to import data from a previous Telegram account used on this device.";
+"WebApp.ImportData.Description" = "**%@** is requesting permission to import data from a previous WinterGram account used on this device.";
"WebApp.ImportData.AccountHeader" = "ACCOUNT TO IMPORT DATA FROM";
"WebApp.ImportData.CreatedOn" = "created on %@";
"WebApp.ImportData.Import" = "Import";
@@ -14145,10 +14145,10 @@ Sorry for the inconvenience.";
"VideoChat.RevokeLink" = "Revoke Link";
-"InviteLink.GroupCallLinkHelp" = "Anyone on Telegram can join your call by following the link below.";
+"InviteLink.GroupCallLinkHelp" = "Anyone on WinterGram can join your call by following the link below.";
"InviteLink.CallLinkTitle" = "Call Link";
"InviteLink.CreatedGroupCallFooter" = "Be the first to join the call and add people from there. [Open Call >](open_call)";
-"InviteLink.QRCode.InfoGroupCall" = "Everyone on Telegram can scan this code to join your group call.";
+"InviteLink.QRCode.InfoGroupCall" = "Everyone on WinterGram can scan this code to join your group call.";
"Call.GenericGroupCallTitle" = "Group Call";
@@ -14320,7 +14320,7 @@ Sorry for the inconvenience.";
"Premium.CreateMultipleStories" = "Create Multiple Stories";
-"FrozenAccount.Violation.TextNew" = "Your account was frozen for breaking Telegram's [Terms and Conditions]().";
+"FrozenAccount.Violation.TextNew" = "Your account was frozen for breaking WinterGram's [Terms and Conditions]().";
"FrozenAccount.Violation.TextNew_URL" = "https://telegram.org/tos";
"Stars.Purchase.BuyStarGiftInfo" = "Buy Stars to acquire a unique collectible.";
@@ -14364,7 +14364,7 @@ Sorry for the inconvenience.";
"Chat.TrimVoiceMessageToResume.Text" = "Audio outside that range will be discarded, and recording will start immediately.";
"Chat.TrimVoiceMessageToResume.Proceed" = "Proceed";
-"Contacts.LimitedAccess.Text" = "You have limited Telegram from accessing all of your contacts.";
+"Contacts.LimitedAccess.Text" = "You have limited WinterGram from accessing all of your contacts.";
"Contacts.LimitedAccess.Manage" = "MANAGE";
"Media.PhotoHdOn" = "The photo will be sent in high quality.";
@@ -14517,7 +14517,7 @@ Sorry for the inconvenience.";
"Chat.Todo.ContextMenu.SectionList" = "List";
"Chat.Todo.ContextMenu.SectionsInfo" = "You're viewing actions for one task.\nYou can switch to actions for the list.";
-"Chat.Todo.PremiumRequired" = "Only [Telegram Premium]() subscribers can mark tasks as done.";
+"Chat.Todo.PremiumRequired" = "Only [WinterGram Premium]() subscribers can mark tasks as done.";
"Chat.Todo.CompletionLimited" = "%@ has restricted others from editing this checklist.";
"Chat.Todo.CompletionLimitedForward" = "You can’t make changes to forwarded checklists.";
@@ -14608,10 +14608,10 @@ Sorry for the inconvenience.";
"BalanceNeeded.FragmentSubtitle" = "You can add funds to your balance via the third-party platform Fragment.";
"BalanceNeeded.FragmentAction" = "Add Funds via Fragment";
-"Settings.Ton.Description" = "Use TON to submit post suggestions to channels on Telegram.";
+"Settings.Ton.Description" = "Use TON to submit post suggestions to channels on WinterGram.";
"Settings.TransactionsTon.Title" = "TON";
-"Settings.TransactionsTon.Subtitle" = "Use TON to submit post suggestions to channels on Telegram.";
+"Settings.TransactionsTon.Subtitle" = "Use TON to submit post suggestions to channels on WinterGram.";
"Chat.DeletePaidMessageStars.Title" = "Stars Will Be Lost";
"Chat.DeletePaidMessageStars.Text" = "You won't receive **Stars** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication.";
@@ -14675,13 +14675,13 @@ Sorry for the inconvenience.";
"Stars.SellGiftMinAmountToast.Text" = "You cannot sell gift for less than %@";
"Premium.Week.SignUp" = "Sign up for %@";
-"Premium.Week.SignUpInfo" = "Get Telegram Premium for 1 week";
+"Premium.Week.SignUpInfo" = "Get WinterGram Premium for 1 week";
"Settings.MyTon" = "My TON";
-"Stars.Gift.Ton.Text" = "Use TON to submit post suggestions to channels on Telegram.";
+"Stars.Gift.Ton.Text" = "Use TON to submit post suggestions to channels on WinterGram.";
"Stars.Ton.Title" = "TON";
-"Stars.Ton.Description" = "Use TON to submit post suggestions to channels on Telegram.";
+"Stars.Ton.Description" = "Use TON to submit post suggestions to channels on WinterGram.";
"Notification.Ton.Subtitle" = "Use TON to submit post suggestions to channels.";
"Notification.Ton.SubtitleYou" = "With TON, %@ will be able to submit post suggestions to channels.";
@@ -14709,7 +14709,7 @@ Sorry for the inconvenience.";
"Chat.SensitiveContentShort" = "18+";
-"AccessDenied.AgeVerificationCamera" = "Telegram needs access to your camera for age verification.\n\nOpen your device's Settings > Privacy > Camera and set Telegram to ON.";
+"AccessDenied.AgeVerificationCamera" = "WinterGram needs access to your camera for age verification.\n\nOpen your device's Settings > Privacy > Camera and set WinterGram to ON.";
"PeerInfo.Gifts.Collections.All" = "All Gifts";
"PeerInfo.Gifts.Collections.Add" = "Add Collection";
@@ -14756,8 +14756,8 @@ Sorry for the inconvenience.";
"Gift.Options.Gift.BuyLimitReached_any" = "You've already sent %@ of these gifts, and it's the limit.";
"AgeVerification.Title" = "Age Verification";
-"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
-"AgeVerification.Text.GB" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
+"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by WinterGram.";
+"AgeVerification.Text.GB" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by WinterGram.";
"AgeVerification.Verify" = "Verify My Age";
"AgeVerification.Unavailable.Title" = "18+";
@@ -14793,7 +14793,7 @@ Sorry for the inconvenience.";
"Gift.Buy.PayInTon.Tooltip" = "Pay with TON to skip the 21-day wait before transferring the gift again.";
"Premium.PremiumGift.Title" = "Premium Gift";
-"Premium.PremiumGift.Description" = "Subscribe to **Telegram Premium** to send up to **5** of these gifts and unlock access to multiple additional features.";
+"Premium.PremiumGift.Description" = "Subscribe to **WinterGram Premium** to send up to **5** of these gifts and unlock access to multiple additional features.";
"Stars.SellGift.TonAmountTitle" = "PRICE IN TON";
"Stars.SellGift.OnlyTon" = "Only Accept TON";
@@ -14849,7 +14849,7 @@ Sorry for the inconvenience.";
"Stories.DeleteAlbum.Confirmation" = "Delete %@?";
"ProfileLevelInfo.Title" = "Rating";
-"ProfileLevelInfo.MyText" = "The rating reflects your activity on Telegram. What affects it:";
+"ProfileLevelInfo.MyText" = "The rating reflects your activity on WinterGram. What affects it:";
"ProfileLevelInfo.MyDescriptionToday_1" = "The rating updates today.\n\%@ point is pending.";
"ProfileLevelInfo.MyDescriptionToday_any" = "The rating updates today.\n\%@ points are pending.";
"ProfileLevelInfo.MyDescriptionDays_1" = "%@ day";
@@ -14866,13 +14866,13 @@ Sorry for the inconvenience.";
"ProfileLevelInfo.MyDescriptionInPreviewToday_1" = "This will be your rating today,\n after %@ is added. [Back >]()";
"ProfileLevelInfo.MyDescriptionInPreviewToday_any" = "This will be your rating today,\n after %@ added. [Back >]()";
-"ProfileLevelInfo.OtherDescription" = "The rating reflects **%@'s** activity on Telegram. What affects it:";
+"ProfileLevelInfo.OtherDescription" = "The rating reflects **%@'s** activity on WinterGram. What affects it:";
"ProfileLevelInfo.LevelIndex_1" = "Level %@";
"ProfileLevelInfo.LevelIndex_any" = "Level %@";
"ProfileLevelInfo.CloseButton" = "Understood";
-"ProfileLevelInfo.Item0.Title" = "Gifts from Telegram";
-"ProfileLevelInfo.Item0.Text" = "100% of the Stars spent on gifts purchased from Telegram.";
+"ProfileLevelInfo.Item0.Title" = "Gifts from WinterGram";
+"ProfileLevelInfo.Item0.Text" = "100% of the Stars spent on gifts purchased from WinterGram.";
"ProfileLevelInfo.Item0.Badge" = "ADDED";
"ProfileLevelInfo.Item1.Title" = "Gifts and Posts from Users";
"ProfileLevelInfo.Item1.Text" = "20% of the Stars spent on gifts or posts from users and channels.";
@@ -14906,7 +14906,7 @@ Sorry for the inconvenience.";
"Gift.Upgrade.GiftTitle" = "Make Unique";
"Gift.Upgrade.GiftDescription" = "Let %@ turn this gift into a unique collectible.";
"Gift.Upgrade.Unique.GiftDescription" = "%@ will get a unique number, model, backdrop, and symbol for the gift.";
-"Gift.Upgrade.Transferable.GiftDescription" = "%@ will be able to send the gift to anyone Telegram.";
+"Gift.Upgrade.Transferable.GiftDescription" = "%@ will be able to send the gift to anyone WinterGram.";
"Gift.Upgrade.Tradable.GiftDescription" = "%@ will be able to auction the gift on third-party NFT marketplaces.";
"Notification.StarGift.Subtitle.Upgrade.Prepaid" = "%@ can now turn this gift to a unique collectible";
@@ -14988,13 +14988,13 @@ Sorry for the inconvenience.";
"Gift.Upgrade.Skip" = "Skip";
"Gift.Upgrade.UpgradeNext" = "Upgrade Next Gift";
-"Gift.Value.DescriptionAveragePrice" = "This is the average sale price of **%@** on Telegram and Fragment over the past month.";
+"Gift.Value.DescriptionAveragePrice" = "This is the average sale price of **%@** on WinterGram and Fragment over the past month.";
"Gift.Value.DescriptionLastPriceFragment" = "This is the last price at which **%@** was last sold on Fragment.";
-"Gift.Value.DescriptionLastPriceTelegram" = "This is the last price at which **%@** was last sold on Telegram.";
+"Gift.Value.DescriptionLastPriceTelegram" = "This is the last price at which **%@** was last sold on WinterGram.";
-"Gift.Value.LastPriceInfo" = "**%1$@** is the last price for %2$@ gifts listed on Telegram and Fragment.";
-"Gift.Value.MinimumPriceInfo" = "**%1$@** is the floor price for %2$@ gifts listed on Telegram and Fragment.";
-"Gift.Value.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts listed on Telegram and Fragment over the past month.";
+"Gift.Value.LastPriceInfo" = "**%1$@** is the last price for %2$@ gifts listed on WinterGram and Fragment.";
+"Gift.Value.MinimumPriceInfo" = "**%1$@** is the floor price for %2$@ gifts listed on WinterGram and Fragment.";
+"Gift.Value.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts listed on WinterGram and Fragment over the past month.";
"Gift.Value.InitialSale" = "Initial Sale";
"Gift.Value.InitialPrice" = "Initial Price";
@@ -15002,7 +15002,7 @@ Sorry for the inconvenience.";
"Gift.Value.LastPrice" = "Last Price";
"Gift.Value.MinimumPrice" = "Minimum Price";
"Gift.Value.AveragePrice" = "Average Price";
-"Gift.Value.ForSaleOnTelegram" = "for sale on Telegram";
+"Gift.Value.ForSaleOnTelegram" = "for sale on WinterGram";
"Gift.Value.ForSaleOnFragment" = "for sale on Fragment";
"Gift.View.Context.SetAsTheme" = "Set as Theme in...";
@@ -15036,7 +15036,7 @@ Locale: %3$@
Target phone: %4$@
App: %5$@
-App version: %6$@
+App version: %6$@
Issue: %7$@
Error: %8$@";
@@ -15129,10 +15129,10 @@ Error: %8$@";
"MESSAGE_SUGGEST_BIRTHDAY" = "%@ suggested you your birthday";
"Gift.UnavailableAction.Title" = "Action Locked";
-"Gift.UnavailableAction.Text" = "Transfer this gift to your Telegram account on Fragment to unlock this action.";
+"Gift.UnavailableAction.Text" = "Transfer this gift to your WinterGram account on Fragment to unlock this action.";
"Gift.UnavailableAction.OpenFragment" = "Open Fragment";
-"Gift.Unique.Telegram" = "Telegram";
+"Gift.Unique.Telegram" = "WinterGram";
"VoiceChat.ContextEnableMessages" = "Enable Messages";
"VoiceChat.ContextDisableMessages" = "Disable Messages";
@@ -15151,7 +15151,7 @@ Error: %8$@";
"ProfileLevelInfo.NegativeRating" = "Negative rating";
-"Notification.StarsGift.Assigned" = "You started displaying %@ on your Telegram profile page.";
+"Notification.StarsGift.Assigned" = "You started displaying %@ on your WinterGram profile page.";
"Gift.View.SenderInfo" = "**%@** sent you this gift on **%@**.";
@@ -15196,7 +15196,7 @@ Error: %8$@";
"Gift.Setup.AuctionInfo.Bidders_1" = "%@ bidder";
"Gift.Setup.AuctionInfo.Bidders_any" = "%@ bidders";
-"PrivacySettings.LoginEmailSetupInfo" = "Setup your email address for Telegram login codes.";
+"PrivacySettings.LoginEmailSetupInfo" = "Setup your email address for WinterGram login codes.";
"LoginEmail.Title" = "Add Email";
"LoginEmail.Description" = "Please add your email address to keep access to your account.";
@@ -15223,7 +15223,7 @@ Error: %8$@";
"Gift.Auction.TimeLeftMinutes" = "{m}:{s} left";
"Gift.Auction.ItemsBought_1" = "item bought";
"Gift.Auction.ItemsBought_any" = "items bought";
-"Gift.Auction.ForSaleOnTelegram" = "for sale on Telegram";
+"Gift.Auction.ForSaleOnTelegram" = "for sale on WinterGram";
"Gift.Auction.ForSaleOnFragment" = "for sale on Fragment";
"Gift.Auction.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts.";
"Gift.Auction.Stars_1" = "%@ Star";
@@ -15363,9 +15363,9 @@ Error: %8$@";
"LiveStreamSettings.SaveSettings" = "Save Settings";
"AddContact.Title" = "New Contact";
-"AddContact.PhoneNumber.IsContact"= "This phone number is already in your contacts. [View >]()";
-"AddContact.PhoneNumber.Registered"= "This phone number is on Telegram.";
-"AddContact.PhoneNumber.NotRegistered"= "This phone number is not on Telegram. [Invite >]()";
+"AddContact.PhoneNumber.IsContact" = "This phone number is already in your contacts. [View >]()";
+"AddContact.PhoneNumber.Registered" = "This phone number is on WinterGram.";
+"AddContact.PhoneNumber.NotRegistered" = "This phone number is not on WinterGram. [Invite >]()";
"AddContact.SyncToPhone" = "Sync Contact to Phone";
"AddContact.NotePlaceholder" = "Add notes only visible to you";
"AddContact.AddQR" = "Add via QR Code";
@@ -15382,7 +15382,7 @@ Error: %8$@";
"ScheduleMessage.RepeatPeriod.Yearly" = "Yearly";
"ScheduleMessage.PremiumRequired.Title" = "Premium Required";
-"ScheduleMessage.PremiumRequired.Text" = "Subscribe to **Telegram Premium** to schedule repeating messages.";
+"ScheduleMessage.PremiumRequired.Text" = "Subscribe to **WinterGram Premium** to schedule repeating messages.";
"ScheduleMessage.PremiumRequired.Add" = "Add";
"Stars.Transaction.GiftAuctionBid" = "Gift Auction Bid";
@@ -15567,11 +15567,11 @@ Error: %8$@";
"Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager.";
"Gift.Demo.Title" = "Unique Collectibles";
-"Gift.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile.";
+"Gift.Demo.Description" = "WinterGram Gifts are collectible items you can trade or showcase on your profile.";
"Gift.Demo.Unique.Title" = "Unique";
"Gift.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol.";
"Gift.Demo.Tradable.Title" = "Tradable";
-"Gift.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces.";
+"Gift.Demo.Tradable.Text" = "Sell your gift on WinterGram or on third-party NFT marketplaces.";
"Gift.Demo.Wearable.Title" = "Wearable";
"Gift.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses.";
"Gift.Demo.Understood" = "Understood";
@@ -15596,7 +15596,7 @@ Error: %8$@";
"Gift.Auction.GiftAuction" = "Gift Auction";
"Gift.Auction.UpcomingAuction" = "Upcoming Auction";
-"Gift.Auction.LearnMore" = "Learn more about Telegram Gifts >";
+"Gift.Auction.LearnMore" = "Learn more about WinterGram Gifts >";
"Gift.AuctionBid.UpcomingTitle" = "Place an Early Bid";
@@ -15621,7 +15621,7 @@ Error: %8$@";
"Gift.WearPreview.Limited" = "limited";
"Gift.WearPreview.Upgraded" = "upgraded";
"Gift.WearPreview.FreeUpgrade" = "Free\nUpgrade";
-"Gift.WearPreview.LearnMore" = "Learn more about wearing Telegram Gifts >";
+"Gift.WearPreview.LearnMore" = "Learn more about wearing WinterGram Gifts >";
"Notification.StarGift.Sold" = "sold";
"Notification.StarGiftOffer.Offer" = "**%1$@** offered you **%2$@** for your gift **%3$@**.";
@@ -15665,7 +15665,7 @@ Error: %8$@";
"CocoonInfo.Private.Title" = "Private";
"CocoonInfo.Private.Text" = "No third party can access any data, such as translations, inside Cocoon.";
"CocoonInfo.Efficient.Title" = "Efficient";
-"CocoonInfo.Efficient.Text" = "Cocoon has allowed Telegram to reduce translation costs by 6x.";
+"CocoonInfo.Efficient.Text" = "Cocoon has allowed WinterGram to reduce translation costs by 6x.";
"CocoonInfo.ForEveryone.Title" = "For Everyone";
"CocoonInfo.ForEveryone.Text" = "Any developer can use Cocoon for AI features. Learn more at [@cocoon](telegram) or [cocoon.org](web).";
"CocoonInfo.IntergrateInfo" = "Want to integrate Cocoon into your projects?\nReach out at [t.me/cocoon?direct]()";
@@ -15682,7 +15682,7 @@ Error: %8$@";
"Conversation.Dice.Change" = "change";
"EmojiStake.Title" = "Emoji Stake";
-"EmojiStake.Description" = "An experimental feature for Telegram Premium users.";
+"EmojiStake.Description" = "An experimental feature for WinterGram Premium users.";
"EmojiStake.ResultsTitle" = "RESULTS AND RETURNS";
"EmojiStake.StreakInfo" = "A streak resets after 3 # or a stake change.";
"EmojiStake.StakeTitle" = "STAKE";
@@ -15693,7 +15693,7 @@ Error: %8$@";
"Conversation.Summary.Text" = "Tap to see original text";
"Conversation.Summary.Limit.Title" = "AI Summary";
-"Conversation.Summary.Limit.Text" = "Summarize large messages with AI – unlimited with Telegram Premium.";
+"Conversation.Summary.Limit.Text" = "Summarize large messages with AI – unlimited with WinterGram Premium.";
"Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter";
@@ -15760,8 +15760,8 @@ Error: %8$@";
"AuthConfirmation.Description" = "This site will receive your **name**,\n**username** and **profile photo**.";
"AuthConfirmation.DescriptionApp" = "This app will receive your **name**,\n**username** and **profile photo**.";
"AuthConfirmation.Device" = "Device";
-"AuthConfirmation.IpAddress" ="IP Address";
-"AuthConfirmation.Info" ="This login attempt came from the device above.";
+"AuthConfirmation.IpAddress" = "IP Address";
+"AuthConfirmation.Info" = "This login attempt came from the device above.";
"AuthConfirmation.AllowMessages" = "Allow Messages";
"AuthConfirmation.AllowMessagesInfo" = "This will allow %@ to message you.";
"AuthConfirmation.Cancel" = "Cancel";
@@ -15871,8 +15871,8 @@ Error: %8$@";
"Gift.Craft.UnavailableBlockchain.Text" = "This gift can't be used as the first one in crafting because it has been recorded on the blockchain.";
"AuthConfirmation.Emoji.Title" = "Tap the emoji shown\non your other device";
-"AuthConfirmation.Emoji.Description" = "Telegram wants to make sure it's really you.";
-"AuthConfirmation.Emoji.DescriptionFirst" = "Telegram wants to make sure it's really you trying to log in to **%@**.";
+"AuthConfirmation.Emoji.Description" = "WinterGram wants to make sure it's really you.";
+"AuthConfirmation.Emoji.DescriptionFirst" = "WinterGram wants to make sure it's really you trying to log in to **%@**.";
"Chat.TagPlaceholder" = "Tag";
@@ -16167,7 +16167,7 @@ Error: %8$@";
"TextProcessing.ActionApply" = "Apply";
"TextProcessing.ActionClose" = "Close";
"TextProcessing.LimitToast.Title" = "Daily limit reached";
-"TextProcessing.LimitToast.Text" = "Get **Telegram Premium** for **50x** more text edits per day.";
+"TextProcessing.LimitToast.Text" = "Get **WinterGram Premium** for **50x** more text edits per day.";
"TextProcessing.Emojify" = "Emojify";
"TextProcessing.TextExpand" = "more";
"TextProcessing.TextCollapse" = "less";
@@ -16225,7 +16225,7 @@ Error: %8$@";
"Attachment.ChatsMusic" = "YOUR CHATS";
"Attachment.PublicMusic" = "PUBLIC CHATS";
-"PeerInfo.UnofficialSecurityRisk" = "%@ uses an unofficial Telegram client – messages to this user may be less secure.";
+"PeerInfo.UnofficialSecurityRisk" = "%@ uses an unofficial WinterGram client – messages to this user may be less secure.";
"Gallery.Live" = "LIVE";
@@ -16277,18 +16277,18 @@ Error: %8$@";
"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Reactions Deleted.";
"Chat.AdminAction.ToastMessagesAndReactionsDeletedText" = "Messages and reactions deleted.";
-"Premium.SignUp.SignUpNewInfo" = "Get Telegram Premium for %@";
+"Premium.SignUp.SignUpNewInfo" = "Get WinterGram Premium for %@";
"Premium.SignUp.SignUpNewInfo.Days_1" = "%@ day";
"Premium.SignUp.SignUpNewInfo.Days_any" = "%@ days";
-"Premium.SignUp.SignUpNewInfoNone" = "Get Telegram Premium";
+"Premium.SignUp.SignUpNewInfoNone" = "Get WinterGram 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.Support.NewText" = "Sign up for a %@ WinterGram Premium subscription to help cover the SMS costs.";
+"Login.Fee.Support.NewTextNone" = "Sign up for WinterGram Premium subscription to help cover the SMS costs.";
-"Login.Fee.GetPremiumNone" = "Get Telegram Premium";
-"Login.Fee.GetPremiumForDays" = "Get Telegram Premium for %@";
+"Login.Fee.GetPremiumNone" = "Get WinterGram Premium";
+"Login.Fee.GetPremiumForDays" = "Get WinterGram Premium for %@";
"Login.Fee.GetPremiumForDays.Days_1" = "%@ day";
"Login.Fee.GetPremiumForDays.Days_any" = "%@ days";
@@ -16364,7 +16364,7 @@ Error: %8$@";
"SocksProxySetup.QrCode.TgLink" = "tg:// link";
"SocksProxySetup.QrCode.TMeLink" = "t.me link";
-"WebBrowser.OpenLinksInfo" = "Open links inside Telegram instead of your default browser for more privacy.";
+"WebBrowser.OpenLinksInfo" = "Open links inside WinterGram instead of your default browser for more privacy.";
"WebBrowser.Exceptions.OpenInApp" = "OPEN IN-APP";
"WebBrowser.Exceptions.DontOpenInApp" = "DON'T OPEN IN-APP";
"WebBrowser.Exceptions.InAppInfo" = "These sites will still be opened in-app.";
@@ -16373,3 +16373,201 @@ Error: %8$@";
"RichTextPreview.Formula" = "[formula]";
"RichTextPreview.Table" = "[table]";
"RichTextPreview.Music" = "Music";
+
+/* WinterGram fork strings (auto-managed) */
+"WinterGram.APIHash" = "API Hash";
+"WinterGram.APIID" = "API ID";
+"WinterGram.APPEARANCE" = "APPEARANCE";
+"WinterGram.AddVisualGift" = "Add Visual Gift";
+"WinterGram.AddVisuallyToProfile" = "Add visually to profile";
+"WinterGram.AllowSavingRestrictedContent" = "Allow Saving Restricted Content";
+"WinterGram.AllowScreenshotsEverywhere" = "Allow Screenshots Everywhere";
+"WinterGram.Always" = "Always";
+"WinterGram.Android" = "Android";
+"WinterGram.Appearance" = "Appearance";
+"WinterGram.ApplyToBubbles" = "Apply to Bubbles";
+"WinterGram.ApplyToChatList" = "Apply to Chat List";
+"WinterGram.ApplyToNavigationBars" = "Apply to Navigation Bars";
+"WinterGram.ApplyToTabBar" = "Apply to Tab Bar";
+"WinterGram.AutoMarkAsRead" = "Auto Mark as Read";
+"WinterGram.AutoPrivacy" = "Auto Privacy";
+"WinterGram.StashPrivacy.ProfilePhoto" = "Profile Photo";
+"WinterGram.StashPrivacy.PhoneNumber" = "Phone Number";
+"WinterGram.StashPrivacy.Presence" = "Last Seen & Online";
+"WinterGram.StashPrivacy.Forwards" = "Forwarded Messages";
+"WinterGram.StashPrivacy.VoiceCalls" = "Calls";
+"WinterGram.StashPrivacy.Birthday" = "Birthday";
+"WinterGram.StashPrivacy.GiftsAutoSave" = "Gifts";
+"WinterGram.StashPrivacy.Bio" = "Bio";
+"WinterGram.StashPrivacy.SavedMusic" = "Saved Music";
+"WinterGram.StashPrivacy.GroupInvitations" = "Group Invitations";
+"WinterGram.Automatic" = "Automatic";
+"WinterGram.AvatarShape" = "Avatar Shape";
+"WinterGram.AyuGram" = "Ayu";
+"WinterGram.CoreMenu" = "Core";
+"WinterGram.Core" = "Core";
+"WinterGram.BadgeDeveloperRole" = "is a WinterGram developer.";
+"WinterGram.BadgeDeveloperSuffix" = "supported WinterGram development and received a unique badge.";
+"WinterGram.BadgeOfficialChannelSuffix" = "is the official WinterGram resource.";
+"WinterGram.Beta" = "Beta";
+"WinterGram.BotAPI" = "Bot API";
+"WinterGram.BubbleRadius" = "Bubble Radius";
+"WinterGram.CHAT" = "CHAT";
+"WinterGram.AddTemplate" = "Add Template";
+"WinterGram.Channel" = "Channel";
+"WinterGram.Chat" = "Chat";
+"WinterGram.MutualContact" = "Mutual contact";
+"WinterGram.NotMutualContact" = "Not a mutual contact";
+"WinterGram.Other" = "Other";
+"WinterGram.ClearSavedDeletedMessages" = "Clear Saved Deleted Messages";
+"WinterGram.ConfirmGIFs" = "Confirm GIFs";
+"WinterGram.ConfirmStickers" = "Confirm Stickers";
+"WinterGram.ConfirmVoiceMessages" = "Confirm Voice Messages";
+"WinterGram.ConfirmStoryView" = "Confirm Story View";
+"WinterGram.Custom" = "Custom...";
+"WinterGram.CustomFont" = "Custom Font";
+"WinterGram.Default" = "Default";
+"WinterGram.Disabled" = "Disabled";
+"WinterGram.DefaultReaction" = "Default Reaction";
+"WinterGram.DefaultRealDevice" = "Default (Real Device)";
+"WinterGram.DeletedAndEditedMessagesAreKeptLocallyOnThisDeviceOnly" = "Deleted and edited messages are kept locally on this device only.";
+"WinterGram.DeletedMark" = "Deleted Marker";
+"WinterGram.NFTGift" = "NFT";
+"WinterGram.RegularGift" = "Regular Gift";
+"WinterGram.Desktop" = "Desktop";
+"WinterGram.DimDeletedMessages" = "Dim Deleted Messages";
+"WinterGram.ShowDeletionTime" = "Show Deletion Time";
+"WinterGram.TopBanner" = "Top Banner";
+"WinterGram.Solid" = "Solid";
+"WinterGram.Glass" = "Glass";
+"WinterGram.Gradient" = "Gradient";
+"WinterGram.Outline" = "Outline";
+"WinterGram.DisableAds" = "Disable Ads";
+"WinterGram.DisableOpenLinkWarning" = "Disable Open Link Warning";
+"WinterGram.DoNotChange" = "Do Not Change";
+"WinterGram.EmptyNoPasscode" = "Empty = no passcode";
+"WinterGram.EnterNFTLinkEGHttpsTMeNftSlug" = "Enter NFT link (e.g. https://t.me/nft/slug)";
+"WinterGram.EnterPasscode" = "Enter Passcode";
+"WinterGram.Error" = "Error";
+"WinterGram.InvalidNFTLink" = "Invalid NFT link";
+"WinterGram.Everything" = "Everything";
+"WinterGram.ExteraGram" = "exteraGram";
+"WinterGram.FEATURES" = "FEATURES";
+"WinterGram.Features" = "Features";
+"WinterGram.ForwardWithoutAuthor" = "Forward Without Author";
+"WinterGram.GHOSTMODE" = "GHOST MODE";
+"WinterGram.GhostMode" = "Ghost Mode";
+"WinterGram.GiftAddedVisuallyToProfile" = "Gift added visually to profile";
+"WinterGram.GitHub" = "GitHub";
+"WinterGram.Google" = "Google";
+"WinterGram.HIDDENARCHIVE" = "HIDDEN ARCHIVE";
+"WinterGram.HISTORY" = "HISTORY";
+"WinterGram.Hidden" = "Hidden";
+"WinterGram.HiddenArchive" = "Hidden Archive";
+"WinterGram.HideEditedMark" = "Hide \"edited\" Mark";
+"WinterGram.HideFromStashedPeers" = "Hide From Stashed Peers";
+"WinterGram.HidePremiumStatuses" = "Hide Premium Statuses";
+"WinterGram.HideStories" = "Hide Stories";
+"WinterGram.History" = "History";
+"WinterGram.INFORMATION" = "INFORMATION";
+"WinterGram.IOS" = "iOS";
+"WinterGram.IconPack" = "Icon Pack";
+"WinterGram.InGhostMode" = "In Ghost Mode";
+"WinterGram.IncreaseWebViewHeight" = "Increase WebView Height";
+"WinterGram.Information" = "Information";
+"WinterGram.InfoFooter" = "WinterGram · independent iOS client · GPLv2 © reekeer\n\nWntGram Beta · v1.0.0";
+"WinterGram.LIQUIDGLASS" = "LIQUID GLASS";
+"WinterGram.LiquidGlass" = "Liquid Glass";
+"WinterGram.LinksFooter" = "WinterGram\nVersion: Beta v1.0.0";
+"WinterGram.LocalPremium" = "Local Premium";
+"WinterGram.LocalPremiumUnlocksPremiumOnlyUIOnThisDeviceItDoesNotGrantServerSidePremium" = "Local Premium unlocks Premium-only UI on this device; it does not grant server-side Premium.";
+"WinterGram.MacOS" = "macOS";
+"WinterGram.MaterialDesign" = "Material Design";
+"WinterGram.MessageTranslation" = "Message Translation";
+"WinterGram.Messages" = "Messages";
+"WinterGram.MonospaceFont" = "Monospace Font";
+"WinterGram.MuteNotifications" = "Mute Notifications";
+"WinterGram.Never" = "Never";
+"WinterGram.Off" = "Off";
+"WinterGram.OnlineTracker" = "Online Tracker";
+"WinterGram.OnlyAddedEmojiStickers" = "Only Added Emoji & Stickers";
+"WinterGram.PrivacyFirstMessagingClient" = "Privacy-first messaging client";
+"WinterGram.ProfileName" = "Profile Name";
+"WinterGram.Plugins" = "Plugins";
+"WinterGram.Restart" = "Restart";
+"WinterGram.RestartRequired" = "Restart Required";
+"WinterGram.Round" = "Round";
+"WinterGram.Rounded" = "Rounded";
+"WinterGram.Rounding" = "Rounding";
+"WinterGram.SPOOFING" = "SPOOFING";
+"WinterGram.SaveCurrentAsProfile" = "Save Current as Profile +";
+"WinterGram.SaveDeletedFromBots" = "Save Deleted from Bots";
+"WinterGram.SaveDeletedMessages" = "Save Deleted Messages";
+"WinterGram.SaveEditHistory" = "Save Edit History";
+"WinterGram.SaveSelfDestructMessages" = "Save Self-Destruct Messages";
+"WinterGram.SendWithoutSound" = "Send Without Sound";
+"WinterGram.ShowMessageSeconds" = "Show Message Seconds";
+"WinterGram.ShowPeerID" = "Show Peer ID";
+"WinterGram.ShowRegistrationDate" = "Show Registration Date";
+"WinterGram.SingleCornerRadius" = "Single Corner Radius";
+"WinterGram.SomeSettingsWillTakeEffectAfterRestart" = "Some settings will take effect after restart.";
+"WinterGram.SpoofAppVersion" = "App Version";
+"WinterGram.SpoofDeviceModel" = "Device Model";
+"WinterGram.SpoofTheDeviceModelAppVersionAndWebViewPlatformReportedToTelegramAndMiniAppsAPIIDHashUseYourOwnCredentialsFromMyTelegramOrgChangingThemRequiresReLogin" = "Spoof the device model, app version and WebView platform reported to Telegram and Mini Apps. API ID/Hash use your own credentials from my.telegram.org — changing them requires re-login.";
+"WinterGram.Spoofing" = "Spoofing";
+"WinterGram.Square" = "Square";
+"WinterGram.Squircle" = "Squircle";
+"WinterGram.StashPasscode" = "Stash Passcode";
+"WinterGram.StashedChats" = "Stashed Chats";
+"WinterGram.StashedChatsAreHiddenFromTheMainListAndAccessibleOnlyHere" = "Stashed chats are hidden from the main list and accessible only here.";
+"WinterGram.HiddenArchiveInfo" = "Chats in the hidden archive are not shown in the chat list. Long-press a chat in the list to stash or unstash it.";
+"WinterGram.HiddenArchiveEmpty" = "The hidden archive is empty.";
+"WinterGram.Stories" = "Stories";
+"WinterGram.System" = "System";
+"WinterGram.Telegram" = "Telegram";
+"WinterGram.TelegramAPI" = "Telegram API";
+"WinterGram.Templates" = "Templates";
+"WinterGram.TheseOptionsChangeHowMessageMetadataAndActionsAreShownInChats" = "These options change how message metadata and actions are shown in chats.";
+"WinterGram.TrackOnlineStatus" = "Track Online Status";
+"WinterGram.TranslationProvider" = "Translation Provider";
+"WinterGram.UseScheduledMessages" = "Use Scheduled Messages";
+"WinterGram.TransparencyBlurAndTintCanBeFineTunedPerSurfaceTurnLiquidGlassOffForTheStandardOpaqueLook" = "Transparency, blur and tint can be fine-tuned per surface. Turn Liquid Glass off for the standard opaque look.";
+"WinterGram.UseDefaultTelegramBranding" = "Use Default Branding";
+"WinterGram.Version" = "Version";
+"WinterGram.Vibrancy" = "Vibrancy";
+"WinterGram.VisualGift" = "Visual Gift";
+"WinterGram.WebViewPlatform" = "WebView Platform";
+"WinterGram.WhenGhostModeIsOnWinterGramStopsSendingReadReceiptsOnlineStatusAndTypingActivity" = "When Ghost Mode is on, WinterGram stops sending read receipts, online status and typing activity.";
+"WinterGram.WinterGram" = "WinterGram";
+"WinterGram.WinterGramWntIsAPrivacyFocusedMessagingClientForIPhoneANativePortOfTheAyuGramExperienceItAddsGhostModeSavedDeletedMessagesAndEditHistoryAHiddenArchiveLocalPremiumAdRemovalDeepCustomizationAndLiquidGlass" = "Modified Telegram client.";
+"WinterGram.Yandex" = "Yandex";
+"WinterGram.None" = "None";
+"WinterGram.AppearanceFooter" = "Avatar and bubble shapes, fonts and the app icon pack apply across the whole app.";
+"WinterGram.Categories" = "Categories";
+"WinterGram.LearnMore" = "Learn More";
+"WinterGram.Links" = "Links";
+"WinterGram.Releases" = "Releases";
+"WinterGram.DeletedMessages.Title" = "Deleted Messages";
+"WinterGram.DeletedMessages.Total" = "Total";
+"WinterGram.DeletedMessages.SelectTypes" = "Select Types";
+"WinterGram.DeletedMessages.DeleteSelected" = "Delete Selected";
+"WinterGram.DeletedMessages.ConfirmDelete" = "Delete the selected saved deleted messages? This cannot be undone.";
+"WinterGram.DeletedMessages.Deleted" = "%@ freed";
+"WinterGram.DeletedMessages.Text" = "Messages";
+"WinterGram.DeletedMessages.Photos" = "Photos";
+"WinterGram.DeletedMessages.Videos" = "Videos";
+"WinterGram.DeletedMessages.Voice" = "Voice Messages";
+"WinterGram.DeletedMessages.VideoMessages" = "Video Messages";
+"WinterGram.DeletedMessages.Music" = "Music";
+"WinterGram.DeletedMessages.Stickers" = "Stickers";
+"WinterGram.DeletedMessages.Other" = "Other";
+"WinterGram.DeletedMessages.TopChats" = "Top Chats";
+"WinterGram.ReadAfterAction" = "Read after action";
+"WinterGram.DontReadMessages" = "Don't read messages";
+"WinterGram.DontReadStories" = "Don't read stories";
+"WinterGram.DontSendOnline" = "Don't send «online»";
+"WinterGram.DontSendTyping" = "Don't send «typing»";
+"WinterGram.AutoOffline" = "Auto offline";
+"WinterGram.SPYMODE" = "SPY MODE";
+"WinterGram.SpyMode" = "Spy mode";
+"WinterGram.DeleteSelected" = "Delete Selected";
diff --git a/Telegram/Telegram-iOS/es.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/es.lproj/InfoPlist.strings
index 672fbd1a9c..09af1ca6de 100644
--- a/Telegram/Telegram-iOS/es.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/es.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram subirá continuamente tus contactos a sus servidores fuertemente cifrados, para permitirte interactuar con tus amigos en todos tus dispositivos.";
-"NSLocationWhenInUseUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Cuando eliges compartir tu ubicación en tiempo real con amigos en un chat, Telegram necesita acceso en segundo plano a tu ubicación para mantenerla actualizada mientras la función esté en uso.";
-"NSLocationAlwaysUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa. También es requerido para enviar ubicaciones desde un Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram subirá continuamente tus contactos a sus servidores fuertemente cifrados, para permitirte interactuar con tus amigos en todos tus dispositivos.";
+"NSLocationWhenInUseUsageDescription" = "Cuando envías tu ubicación a tus amigos, WinterGram necesita acceso para mostrarles un mapa.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Cuando eliges compartir tu ubicación en tiempo real con amigos en un chat, WinterGram necesita acceso en segundo plano a tu ubicación para mantenerla actualizada mientras la función esté en uso.";
+"NSLocationAlwaysUsageDescription" = "Cuando envías tu ubicación a tus amigos, WinterGram necesita acceso para mostrarles un mapa. También es requerido para enviar ubicaciones desde un Apple Watch.";
"NSCameraUsageDescription" = "Necesitamos esto para que puedas tomar y compartir fotos y videos, así como para realizar videollamadas.";
"NSPhotoLibraryUsageDescription" = "Es requerido para que puedas compartir fotos y vídeos desde tu biblioteca de fotos.";
"NSPhotoLibraryAddUsageDescription" = "Necesitamos esto para que puedas guardar fotos y videos en tu biblioteca de fotos.";
diff --git a/Telegram/Telegram-iOS/fr.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/fr.lproj/InfoPlist.strings
index fa15291c28..f960d40a3e 100644
--- a/Telegram/Telegram-iOS/fr.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/fr.lproj/InfoPlist.strings
@@ -1,12 +1,12 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram va synchroniser en continu vos contacts sur ses serveurs chiffrés pour vous permettre de joindre vos amis sur tous vos appareils.";
-"NSLocationWhenInUseUsageDescription" = "Quand vous envoyez votre position à vos amis, Telegram doit accéder à votre position pour leur montrer une carte.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quand vous décidez de partager votre position en temps réel dans un échange, Telegram doit y accéder en arrière-plan pour l'actualiser pendant la durée du partage.";
-"NSLocationAlwaysUsageDescription" = "Quand vous décidez de partager votre position en temps réel dans un échange, Telegram doit y accéder en arrière-plan pour l'actualiser pendant la durée du partage. Cela sert aussi à envoyer une position depuis l'Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram va synchroniser en continu vos contacts sur ses serveurs chiffrés pour vous permettre de joindre vos amis sur tous vos appareils.";
+"NSLocationWhenInUseUsageDescription" = "Quand vous envoyez votre position à vos amis, WinterGram doit accéder à votre position pour leur montrer une carte.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quand vous décidez de partager votre position en temps réel dans un échange, WinterGram doit y accéder en arrière-plan pour l'actualiser pendant la durée du partage.";
+"NSLocationAlwaysUsageDescription" = "Quand vous décidez de partager votre position en temps réel dans un échange, WinterGram doit y accéder en arrière-plan pour l'actualiser pendant la durée du partage. Cela sert aussi à envoyer une position depuis l'Apple Watch.";
"NSCameraUsageDescription" = "Nous en avons besoin pour que vous puissiez prendre et partager des photos et des vidéos.";
"NSPhotoLibraryUsageDescription" = "Nous en avons besoin pour que vous puissiez partager des photos et des vidéos de votre photothèque.";
"NSPhotoLibraryAddUsageDescription" = "Nous en avons besoin pour que vous puissiez enregistrer des photos et des vidéos dans votre photothèque.";
"NSMicrophoneUsageDescription" = "Nous en avons besoin pour que vous puissiez enregistrer et partager des messages vocaux et des vidéos avec du son.";
"NSSiriUsageDescription" = "Vous pouvez utiliser Siri pour envoyer des messages.";
-"NSFaceIDUsageDescription" = "Vous pouvez déverrouiller Telegram avec Face ID.";
+"NSFaceIDUsageDescription" = "Vous pouvez déverrouiller WinterGram avec Face ID.";
diff --git a/Telegram/Telegram-iOS/id.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/id.lproj/InfoPlist.strings
index 8291c51d7f..325e026f04 100644
--- a/Telegram/Telegram-iOS/id.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/id.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram akan terus mengunggah kontak Anda ke penyimpanan awan yang dienkripsi penuh, sehingga Anda dapat terhubung dengan teman Anda di semua perangkat Anda.";
-"NSLocationWhenInUseUsageDescription" = "Ketika Anda mengirim lokasi untuk teman, Telegram membutuhkan akses untuk berbagi peta.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Jika Anda memilih untuk membagikan Live Location dengan teman dalam obrolan, Telegram memerlukan akses latar belakang ke lokasi Anda agar lokasi tetap diperbarui selama lokasi langsung dibagikan.";
-"NSLocationAlwaysUsageDescription" = "Jika Anda ingin berbagi Live Location dengan teman dalam chat, Telegram perlu akses latar belakang ke lokasi Anda agar lokasi tetap diperbarui selama lokasi langsung dibagikan. Hal yang sama perlu dilakukan untuk berbagi lokasi dari Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram akan terus mengunggah kontak Anda ke penyimpanan awan yang dienkripsi penuh, sehingga Anda dapat terhubung dengan teman Anda di semua perangkat Anda.";
+"NSLocationWhenInUseUsageDescription" = "Ketika Anda mengirim lokasi untuk teman, WinterGram membutuhkan akses untuk berbagi peta.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Jika Anda memilih untuk membagikan Live Location dengan teman dalam obrolan, WinterGram memerlukan akses latar belakang ke lokasi Anda agar lokasi tetap diperbarui selama lokasi langsung dibagikan.";
+"NSLocationAlwaysUsageDescription" = "Jika Anda ingin berbagi Live Location dengan teman dalam chat, WinterGram perlu akses latar belakang ke lokasi Anda agar lokasi tetap diperbarui selama lokasi langsung dibagikan. Hal yang sama perlu dilakukan untuk berbagi lokasi dari Apple Watch.";
"NSCameraUsageDescription" = "Kami membutuhkan hal ini agar Anda dapat mengambil dan membagikan foto dan video.";
"NSPhotoLibraryUsageDescription" = "Kami membutuhkan hal ini agar Anda dapat berbagi foto dan video dari galeri foto Anda.";
"NSPhotoLibraryAddUsageDescription" = "Kami membutuhkan hal ini agar Anda dapat menyimpan foto dan video ke galeri foto.";
diff --git a/Telegram/Telegram-iOS/it.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/it.lproj/InfoPlist.strings
index a9a5ed1752..40c8b16dfb 100644
--- a/Telegram/Telegram-iOS/it.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/it.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram caricherà continuamente i tuoi contatti sui suoi server cloud altamente criptati per farti connettere con i tuoi amici da tutti i tuoi dispositivi.";
-"NSLocationWhenInUseUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa.";
-"NSLocationAlwaysUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa. Ti serve anche per inviare posizioni da Apple Watch.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando scegli di condividere la tua Posizione Attuale con gli amici in una chat, Telegram ha bisogno dell'accesso in background alla tua posizione per tenerli aggiornati durante la durata della condivisione della posizione.";
+"NSContactsUsageDescription" = "WinterGram caricherà continuamente i tuoi contatti sui suoi server cloud altamente criptati per farti connettere con i tuoi amici da tutti i tuoi dispositivi.";
+"NSLocationWhenInUseUsageDescription" = "Quando invii la tua posizione ai tuoi amici, WinterGram ha bisogno di accedere per mostrare loro la mappa.";
+"NSLocationAlwaysUsageDescription" = "Quando invii la tua posizione ai tuoi amici, WinterGram ha bisogno di accedere per mostrare loro la mappa. Ti serve anche per inviare posizioni da Apple Watch.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando scegli di condividere la tua Posizione Attuale con gli amici in una chat, WinterGram ha bisogno dell'accesso in background alla tua posizione per tenerli aggiornati durante la durata della condivisione della posizione.";
"NSCameraUsageDescription" = "Ci serve per farti scattare, registrare e condividere foto e video, oltre che per fare videochiamate.";
"NSPhotoLibraryUsageDescription" = "Ci serve per farti condividere foto e video dalla tua libreria foto.";
"NSPhotoLibraryAddUsageDescription" = "Ci serve per farti salvare foto e video nella tua libreria foto.";
diff --git a/Telegram/Telegram-iOS/ko.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ko.lproj/InfoPlist.strings
index b692e0af6c..2e9fdcbfda 100644
--- a/Telegram/Telegram-iOS/ko.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/ko.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"CFBundleDisplayName" = "텔레그램";
-"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
-"NSLocationWhenInUseUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다.";
-"NSLocationAlwaysUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다. 애플워치에 위치 전송을 위해서도 필요합니다.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing.";
+"CFBundleDisplayName" = "WinterGram";
+"NSContactsUsageDescription" = "WinterGram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
+"NSLocationWhenInUseUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 WinterGram이 위치 정보에 접근할 수 있어야 합니다.";
+"NSLocationAlwaysUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 WinterGram이 위치 정보에 접근할 수 있어야 합니다. 애플워치에 위치 전송을 위해서도 필요합니다.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, WinterGram needs background access to your location to keep them updated for the duration of the live sharing.";
"NSCameraUsageDescription" = "사진과 비디오 촬영을 위하여 필요합니다.";
"NSPhotoLibraryUsageDescription" = "촬영한 사진과 비디오를 공유하기 위하여 필요합니다.";
"NSPhotoLibraryAddUsageDescription" = "사진과 동영상을 갤러리에 저장하기 위해 이 권한이 필요합니다.";
diff --git a/Telegram/Telegram-iOS/ms.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ms.lproj/InfoPlist.strings
index c6d7bf72f9..880821efd5 100644
--- a/Telegram/Telegram-iOS/ms.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/ms.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram akan sentiasa muat naik kenalan anda ke pelayan awan yang berenkripsi tinggi agar anda boleh berhubung dengan rakan anda di semua peranti anda.";
-"NSLocationWhenInUseUsageDescription" = "Bila anda hantar lokasi anda kepada rakan anda, Telegram perlukan akses untuk tunjuk peta.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Bila anda pilih untuk kongsi Lokasi Langsung anda dengan rakan dalam chat, Telegram perlu akses latar belakang ke lokasi anda agar lokasi anda sentiasa dikemaskini ketika perkongsian.";
-"NSLocationAlwaysUsageDescription" = "Bila anda pilih untuk kongsi lokasi langsung anda dengan rakan dalam chat, Telegram perlu akses latar belakang agar lokasi anda sentiasa dikemaskini. Anda juga harus hantar lokasi anda ke Jam Apple.";
+"NSContactsUsageDescription" = "WinterGram akan sentiasa muat naik kenalan anda ke pelayan awan yang berenkripsi tinggi agar anda boleh berhubung dengan rakan anda di semua peranti anda.";
+"NSLocationWhenInUseUsageDescription" = "Bila anda hantar lokasi anda kepada rakan anda, WinterGram perlukan akses untuk tunjuk peta.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Bila anda pilih untuk kongsi Lokasi Langsung anda dengan rakan dalam chat, WinterGram perlu akses latar belakang ke lokasi anda agar lokasi anda sentiasa dikemaskini ketika perkongsian.";
+"NSLocationAlwaysUsageDescription" = "Bila anda pilih untuk kongsi lokasi langsung anda dengan rakan dalam chat, WinterGram perlu akses latar belakang agar lokasi anda sentiasa dikemaskini. Anda juga harus hantar lokasi anda ke Jam Apple.";
"NSCameraUsageDescription" = "Kita perlukan ini agar anda boleh ambil dan kongsi foto dan video, dan juga buat panggilan video.";
"NSPhotoLibraryUsageDescription" = "Kita perlu ini agar anda boleh kongsi foto dan video dari librari foto anda.";
"NSPhotoLibraryAddUsageDescription" = "Kita perlu ini agar anda boleh simpan foto dan video ke librari foto anda.";
diff --git a/Telegram/Telegram-iOS/nl.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/nl.lproj/InfoPlist.strings
index 1bddf64f04..cdc03c0cce 100644
--- a/Telegram/Telegram-iOS/nl.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/nl.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram synchroniseert je contacten continu naar onze zwaar versleutelde Cloud-servers, zodat je contact kunt opnemen met je vrienden vanaf al je apparaten.";
-"NSLocationWhenInUseUsageDescription" = "Als je je locatie met vrienden wilt delen heeft Telegram toegang nodig om ze een kaart te kunnen tonen.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Als je ervoor kiest om je huidige locatie te delen met vrienden in een chat heeft Telegram achtergrondtoegang tot je locatie nodig om deze bij te werken tijdens het live-delen.";
-"NSLocationAlwaysUsageDescription" = "Telegram heeft toegang nodig om een kaart aan je vrienden te tonen als je jouw locatie met ze deelt. Je hebt dit ook nodig om locaties te sturen vanaf een Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram synchroniseert je contacten continu naar onze zwaar versleutelde Cloud-servers, zodat je contact kunt opnemen met je vrienden vanaf al je apparaten.";
+"NSLocationWhenInUseUsageDescription" = "Als je je locatie met vrienden wilt delen heeft WinterGram toegang nodig om ze een kaart te kunnen tonen.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Als je ervoor kiest om je huidige locatie te delen met vrienden in een chat heeft WinterGram achtergrondtoegang tot je locatie nodig om deze bij te werken tijdens het live-delen.";
+"NSLocationAlwaysUsageDescription" = "WinterGram heeft toegang nodig om een kaart aan je vrienden te tonen als je jouw locatie met ze deelt. Je hebt dit ook nodig om locaties te sturen vanaf een Apple Watch.";
"NSCameraUsageDescription" = "We hebben dit nodig zodat je foto's en video's kunt maken en delen.";
"NSPhotoLibraryUsageDescription" = "We hebben dit nodig zodat je foto's en video's kunt delen vanuit je fotobibliotheek.";
"NSPhotoLibraryAddUsageDescription" = "We hebben dit nodig zodat je foto's en video's kunt opslaan in je fotobibliotheek.";
diff --git a/Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings
index e2edd5f990..7d426c24dc 100644
--- a/Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram będzie nieprzerwanie przesyłać kontakty do silnie zaszyfrowanych serwerów w chmurze, aby umożliwić ci połączenie się ze znajomymi na wszystkich urządzeniach.";
-"NSLocationWhenInUseUsageDescription" = "Gdy wysyłasz swoją lokalizację znajomym, Telegram potrzebuje dostępu, aby pokazać im mapę.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, Telegram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”.";
-"NSLocationAlwaysUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, Telegram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”. Jest to również potrzebne do wysyłania lokalizacji z Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram będzie nieprzerwanie przesyłać kontakty do silnie zaszyfrowanych serwerów w chmurze, aby umożliwić ci połączenie się ze znajomymi na wszystkich urządzeniach.";
+"NSLocationWhenInUseUsageDescription" = "Gdy wysyłasz swoją lokalizację znajomym, WinterGram potrzebuje dostępu, aby pokazać im mapę.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, WinterGram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”.";
+"NSLocationAlwaysUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, WinterGram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”. Jest to również potrzebne do wysyłania lokalizacji z Apple Watch.";
"NSCameraUsageDescription" = "Potrzebujemy tego, aby można było robić i udostępniać zdjęcia i wideo, a także prowadzić rozmowy wideo.";
"NSPhotoLibraryUsageDescription" = "Potrzebujemy tego, aby można było udostępniać zdjęcia i wideo ze swojej biblioteki zdjęć.";
"NSPhotoLibraryAddUsageDescription" = "Potrzebujemy tego, aby można było zapisywać zdjęcia i wideo w swojej bibliotece zdjęć.";
diff --git a/Telegram/Telegram-iOS/pt.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/pt.lproj/InfoPlist.strings
index ddc65bbd02..aa75fe0c66 100644
--- a/Telegram/Telegram-iOS/pt.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/pt.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram atualizará continuamente os seus contatos em servidores na nuvem fortemente criptografados para que você se conecte com seus amigos através de todos os seus dispositivos.";
-"NSLocationWhenInUseUsageDescription" = "Para enviar sua localização para seus amigos o Telegram precisa de permissão para mostrá-los o mapa.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando você escolhe compartilhar sua Localização em Tempo Real com amigos no chat, o Telegram precisa de acesso à sua localização em segundo plano para mantê-los atualizados durante o compartilhamento.";
-"NSLocationAlwaysUsageDescription" = "Para enviar sua localização para seus amigos, o Telegram precisa de permissão para mostrá-los o mapa. Você também precisará disso para enviar localizações de um Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram atualizará continuamente os seus contatos em servidores na nuvem fortemente criptografados para que você se conecte com seus amigos através de todos os seus dispositivos.";
+"NSLocationWhenInUseUsageDescription" = "Para enviar sua localização para seus amigos o WinterGram precisa de permissão para mostrá-los o mapa.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando você escolhe compartilhar sua Localização em Tempo Real com amigos no chat, o WinterGram precisa de acesso à sua localização em segundo plano para mantê-los atualizados durante o compartilhamento.";
+"NSLocationAlwaysUsageDescription" = "Para enviar sua localização para seus amigos, o WinterGram precisa de permissão para mostrá-los o mapa. Você também precisará disso para enviar localizações de um Apple Watch.";
"NSCameraUsageDescription" = "Precisamos acessar sua câmera para que você possa capturar fotos e vídeos.";
"NSPhotoLibraryUsageDescription" = "Precisamos disso para que você possa compartilhar fotos e vídeos de sua galeria.";
"NSPhotoLibraryAddUsageDescription" = "Precisamos disso para que você possa salvar fotos e vídeos em sua galeria de fotos.";
diff --git a/Telegram/Telegram-iOS/ru.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ru.lproj/InfoPlist.strings
index 08c349a3c3..4c2cdbf0ba 100644
--- a/Telegram/Telegram-iOS/ru.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/ru.lproj/InfoPlist.strings
@@ -1,7 +1,7 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства.";
-"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту.";
+"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке WinterGram, чтобы вы могли связаться с друзьями с любого устройства.";
+"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, WinterGram нужно разрешение, чтобы показать им карту.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями.";
"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch.";
"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео.";
diff --git a/Telegram/Telegram-iOS/tr.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/tr.lproj/InfoPlist.strings
index 283b1f2a25..cb8d5cc3c1 100644
--- a/Telegram/Telegram-iOS/tr.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/tr.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram, arkadaşlarınızla tüm cihazlarınız arasında bağlantı kurmanızı sağlamak için kişilerinizi sürekli olarak şifreli bulut sunucularına yükleyecek.";
-"NSLocationWhenInUseUsageDescription" = "Konumunuzu arkadaşlarınıza gönderdiğinizde, Telegram'ın onlara bir harita göstermesi için erişmesi gerekiyor.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Canlı Konumunuzu arkadaşlarınızla bir sohbette paylaşmayı seçtiğinizde, Telegram'ın canlı paylaşım süresince onları güncel tutmak için konumunuza arka plan erişimi olması gerekir.";
-"NSLocationAlwaysUsageDescription" = "Canlı konumunuzu bir sohbette arkadaşlarınızla paylaşmayı seçtiğinizde, Telegram'ın canlı paylaşım süresince konumunuzu güncel tutması için bir arka plan erişimi gerekir. Ayrıca Apple Watch'dan konum göndermek için de buna ihtiyacınız var.";
+"NSContactsUsageDescription" = "WinterGram, arkadaşlarınızla tüm cihazlarınız arasında bağlantı kurmanızı sağlamak için kişilerinizi sürekli olarak şifreli bulut sunucularına yükleyecek.";
+"NSLocationWhenInUseUsageDescription" = "Konumunuzu arkadaşlarınıza gönderdiğinizde, WinterGram'ın onlara bir harita göstermesi için erişmesi gerekiyor.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Canlı Konumunuzu arkadaşlarınızla bir sohbette paylaşmayı seçtiğinizde, WinterGram'ın canlı paylaşım süresince onları güncel tutmak için konumunuza arka plan erişimi olması gerekir.";
+"NSLocationAlwaysUsageDescription" = "Canlı konumunuzu bir sohbette arkadaşlarınızla paylaşmayı seçtiğinizde, WinterGram'ın canlı paylaşım süresince konumunuzu güncel tutması için bir arka plan erişimi gerekir. Ayrıca Apple Watch'dan konum göndermek için de buna ihtiyacınız var.";
"NSCameraUsageDescription" = "Fotoğraf ve video çekip paylaşabilmeniz ve görüntülü arama yapabilmeniz için buna ihtiyacımız var.";
"NSPhotoLibraryUsageDescription" = "Fotoğraf arşivinizdeki fotoğraf ve videoları paylaşabilmeniz için buna ihtiyacımız var.";
"NSPhotoLibraryAddUsageDescription" = "Fotoğraf arşivine fotoğraf ve video kaydedebilmeniz için buna ihtiyacımız var.";
diff --git a/Telegram/Telegram-iOS/uk.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/uk.lproj/InfoPlist.strings
index f9e8f94199..ec88017fdb 100644
--- a/Telegram/Telegram-iOS/uk.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/uk.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram завантажуватиме ваші контакти до зашифрованих хмарних серверів, щоб ви могли зʼєднуватися з друзями на всіх пристроях.";
-"NSLocationWhenInUseUsageDescription" = "Коли ви надсилаєте розташування друзям, Telegram потребує доступу, щоб показати їм мапу.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Коли ви захочете поділитися Маячком із друзями у чаті, Telegram потребує фонового доступу до розташування, щоб воно оновлювалося на час трансляції маячка.";
-"NSLocationAlwaysUsageDescription" = "Коли ви захочете поділитися Маячком із друзями у чаті, Telegram потребує фонового доступу до розташування, щоб воно оновлювалося на час трансляції маячка. Також вам це потрібно для надсилання розташувань з Apple Watch.";
+"NSContactsUsageDescription" = "WinterGram завантажуватиме ваші контакти до зашифрованих хмарних серверів, щоб ви могли зʼєднуватися з друзями на всіх пристроях.";
+"NSLocationWhenInUseUsageDescription" = "Коли ви надсилаєте розташування друзям, WinterGram потребує доступу, щоб показати їм мапу.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Коли ви захочете поділитися Маячком із друзями у чаті, WinterGram потребує фонового доступу до розташування, щоб воно оновлювалося на час трансляції маячка.";
+"NSLocationAlwaysUsageDescription" = "Коли ви захочете поділитися Маячком із друзями у чаті, WinterGram потребує фонового доступу до розташування, щоб воно оновлювалося на час трансляції маячка. Також вам це потрібно для надсилання розташувань з Apple Watch.";
"NSCameraUsageDescription" = "Нам це потрібно, щоб ви могли знімати та ділитися фото й відео.";
"NSPhotoLibraryUsageDescription" = "Нам це потрібно, щоб ви могли ділитися фото та відео з вашої бібліотеки фото.";
"NSPhotoLibraryAddUsageDescription" = "Нам це потрібно, щоб ви могли зберігати фото та відео до вашої бібліотеки фото.";
diff --git a/Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings
index ec5ffb6584..dddd13279b 100644
--- a/Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings
+++ b/Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings
@@ -1,9 +1,9 @@
/* Localized versions of Info.plist keys */
-"NSContactsUsageDescription" = "Telegram barcha qurilmalaringizdan doʻstlaringiz bilan bogʻlana olishingiz uchun muntazam ravishda kontaktlaringizni kuchli shifrlanadigan bulut serverlariga yuklaydi.";
-"NSLocationWhenInUseUsageDescription" = "Joylashuvingizni doʻstlaringizga yuborganingizda Telegram ularga xaritani koʻrsatishi uchun ruxsat kerak boʻladi.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, Telegram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak.";
-"NSLocationAlwaysUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, Telegram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak. Sizga bu Apple Watchdan joylashuvlarni yuborish uchun ham kerak.";
+"NSContactsUsageDescription" = "WinterGram barcha qurilmalaringizdan doʻstlaringiz bilan bogʻlana olishingiz uchun muntazam ravishda kontaktlaringizni kuchli shifrlanadigan bulut serverlariga yuklaydi.";
+"NSLocationWhenInUseUsageDescription" = "Joylashuvingizni doʻstlaringizga yuborganingizda WinterGram ularga xaritani koʻrsatishi uchun ruxsat kerak boʻladi.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, WinterGram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak.";
+"NSLocationAlwaysUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, WinterGram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak. Sizga bu Apple Watchdan joylashuvlarni yuborish uchun ham kerak.";
"NSCameraUsageDescription" = "Bu bizga rasm va videolarga olish, shuningdek, video chaqiruvlar qilish va ularni ulashishingiz uchun kerak.";
"NSPhotoLibraryUsageDescription" = "Bu bizga galereyangizdan rasm va videolaringizni ulasha olishingiz uchun kerak.";
"NSPhotoLibraryAddUsageDescription" = "Bu bizga rasm va videolaringizni galereyangizga saqlay olishingiz uchun kerak.";
diff --git a/Telegram/WatchApp/project.yml b/Telegram/WatchApp/project.yml
index bf08b9408a..7add64f47a 100644
--- a/Telegram/WatchApp/project.yml
+++ b/Telegram/WatchApp/project.yml
@@ -11,7 +11,7 @@
# with project.yml. Do NOT add the container target or SnapshotPreviews here.
name: tgwatch
options:
- bundleIdPrefix: ph.telegra # ph.telegra.Telegraph.watchkitapp, child of the main Telegram app id
+ bundleIdPrefix: dev.reekeer # dev.reekeer.wintergram.watchkitapp, child of the main WinterGram app id
developmentLanguage: en
deploymentTarget:
watchOS: "26.0"
@@ -51,12 +51,12 @@ targets:
info:
path: tgwatch Watch App/Info.plist
properties:
- CFBundleDisplayName: Telegram
+ CFBundleDisplayName: WinterGram
CFBundleIconName: AppIcon
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
WKApplication: true
- WKCompanionAppBundleIdentifier: ph.telegra.Telegraph
+ WKCompanionAppBundleIdentifier: dev.reekeer.wintergram
# Intentional: keeps the watch app independently installable on the watch
# while still shipping inside the Telegram IPA. Do not change to false.
WKRunsIndependentlyOfCompanionApp: true
@@ -68,8 +68,8 @@ targets:
NSLocationWhenInUseUsageDescription: Share your current location in chats.
settings:
base:
- PRODUCT_BUNDLE_IDENTIFIER: ph.telegra.Telegraph.watchkitapp
- DEVELOPMENT_TEAM: C67CF9S4VU
+ PRODUCT_BUNDLE_IDENTIFIER: dev.reekeer.wintergram.watchkitapp
+ DEVELOPMENT_TEAM: REPLACE_WITH_YOUR_TEAM_ID
TARGETED_DEVICE_FAMILY: "4"
GENERATE_INFOPLIST_FILE: "NO"
LD_RUNPATH_SEARCH_PATHS: $(inherited) @executable_path/Frameworks
diff --git a/Telegram/WatchApp/tgwatch Watch App/Info.plist b/Telegram/WatchApp/tgwatch Watch App/Info.plist
index ca62526111..8efc1b7f04 100644
--- a/Telegram/WatchApp/tgwatch Watch App/Info.plist
+++ b/Telegram/WatchApp/tgwatch Watch App/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Telegram
+ WinterGram
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIconName
diff --git a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj
index ac34cf16c3..3c96c16f7b 100644
--- a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj
+++ b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj
@@ -467,7 +467,7 @@
LastUpgradeCheck = 1430;
TargetAttributes = {
F8E880AF01A492CCA4C21162 = {
- DevelopmentTeam = C67CF9S4VU;
+ DevelopmentTeam = REPLACE_WITH_YOUR_TEAM_ID;
};
};
};
@@ -631,11 +631,11 @@
CODE_SIGNING_ALLOWED = NO;
CODE_SIGNING_REQUIRED = NO;
CODE_SIGN_IDENTITY = "";
- DEVELOPMENT_TEAM = C67CF9S4VU;
+ DEVELOPMENT_TEAM = REPLACE_WITH_YOUR_TEAM_ID;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "tgwatch Watch App/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = ph.telegra.Telegraph.watchkitapp;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.reekeer.wintergram.watchkitapp;
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
@@ -653,11 +653,11 @@
CODE_SIGN_IDENTITY = "";
DEAD_CODE_STRIPPING = YES;
DEPLOYMENT_POSTPROCESSING = YES;
- DEVELOPMENT_TEAM = C67CF9S4VU;
+ DEVELOPMENT_TEAM = REPLACE_WITH_YOUR_TEAM_ID;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "tgwatch Watch App/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = ph.telegra.Telegraph.watchkitapp;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.reekeer.wintergram.watchkitapp;
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = YES;
diff --git a/Tests/WinterGram/WinterGramGhostLogicTests.swift b/Tests/WinterGram/WinterGramGhostLogicTests.swift
new file mode 100644
index 0000000000..d591734539
--- /dev/null
+++ b/Tests/WinterGram/WinterGramGhostLogicTests.swift
@@ -0,0 +1,70 @@
+import Foundation
+
+// Standalone unit tests for the ghost-mode decision logic.
+//
+// This project has no Bazel test harness, but WinterGramGhostLogic.swift is
+// dependency-free, so it can be compiled and run directly:
+//
+// swiftc \
+// submodules/TelegramUIPreferences/Sources/WinterGramGhostLogic.swift \
+// Tests/WinterGram/WinterGramGhostLogicTests.swift \
+// -o /tmp/wnt_tests && /tmp/wnt_tests
+//
+// The functions tested here are the exact ones WinterGramSettings forwards to,
+// so a regression in the shipping rules fails these tests.
+
+@main
+struct WinterGramGhostLogicTests {
+ static var failures = 0
+ static var checks = 0
+
+ static func expect(_ actual: Bool, _ expected: Bool, _ label: String) {
+ checks += 1
+ if actual != expected {
+ failures += 1
+ print("FAIL: \(label) — expected \(expected), got \(actual)")
+ }
+ }
+
+ static func main() {
+ let G = WinterGramGhostLogic.self
+
+ // suppressesReadReceipts: only when ghost ON and receipts OFF.
+ expect(G.suppressesReadReceipts(ghostModeEnabled: true, sendReadReceipts: false), true, "readReceipts ghost+off")
+ expect(G.suppressesReadReceipts(ghostModeEnabled: true, sendReadReceipts: true), false, "readReceipts ghost+on")
+ expect(G.suppressesReadReceipts(ghostModeEnabled: false, sendReadReceipts: false), false, "readReceipts noghost+off")
+ expect(G.suppressesReadReceipts(ghostModeEnabled: false, sendReadReceipts: true), false, "readReceipts noghost+on")
+
+ // suppressesOnlinePresence
+ expect(G.suppressesOnlinePresence(ghostModeEnabled: true, sendOnlineStatus: false), true, "online ghost+off")
+ expect(G.suppressesOnlinePresence(ghostModeEnabled: true, sendOnlineStatus: true), false, "online ghost+on")
+ expect(G.suppressesOnlinePresence(ghostModeEnabled: false, sendOnlineStatus: false), false, "online noghost+off")
+
+ // suppressesTypingStatus
+ expect(G.suppressesTypingStatus(ghostModeEnabled: true, sendUploadProgress: false), true, "typing ghost+off")
+ expect(G.suppressesTypingStatus(ghostModeEnabled: false, sendUploadProgress: false), false, "typing noghost+off")
+
+ // suppressesStoryViews
+ expect(G.suppressesStoryViews(ghostModeEnabled: true, sendReadStories: false), true, "story ghost+off")
+ expect(G.suppressesStoryViews(ghostModeEnabled: true, sendReadStories: true), false, "story ghost+on")
+
+ // shouldMarkReadAfterAction: requires reads suppressed AND the toggle on.
+ expect(G.shouldMarkReadAfterAction(ghostModeEnabled: true, sendReadReceipts: false, markReadAfterAction: true), true, "markRead suppressed+toggle")
+ expect(G.shouldMarkReadAfterAction(ghostModeEnabled: true, sendReadReceipts: false, markReadAfterAction: false), false, "markRead suppressed+notoggle")
+ expect(G.shouldMarkReadAfterAction(ghostModeEnabled: true, sendReadReceipts: true, markReadAfterAction: true), false, "markRead notsuppressed+toggle")
+ expect(G.shouldMarkReadAfterAction(ghostModeEnabled: false, sendReadReceipts: false, markReadAfterAction: true), false, "markRead noghost+toggle")
+
+ // shouldGoOfflineAfterAction: requires ghost ON and the toggle on.
+ expect(G.shouldGoOfflineAfterAction(ghostModeEnabled: true, sendOfflineAfterOnline: true), true, "offline ghost+toggle")
+ expect(G.shouldGoOfflineAfterAction(ghostModeEnabled: true, sendOfflineAfterOnline: false), false, "offline ghost+notoggle")
+ expect(G.shouldGoOfflineAfterAction(ghostModeEnabled: false, sendOfflineAfterOnline: true), false, "offline noghost+toggle")
+
+ if failures == 0 {
+ print("OK: all \(checks) ghost-logic checks passed")
+ exit(0)
+ } else {
+ print("\(failures)/\(checks) checks FAILED")
+ exit(1)
+ }
+ }
+}
diff --git a/branding/backplate_badge.png b/branding/backplate_badge.png
new file mode 100644
index 0000000000..4d0a214d0f
Binary files /dev/null and b/branding/backplate_badge.png differ
diff --git a/branding/banner-default.png b/branding/banner-default.png
new file mode 100644
index 0000000000..d2d76eac80
Binary files /dev/null and b/branding/banner-default.png differ
diff --git a/branding/icon-app-dark.png b/branding/icon-app-dark.png
new file mode 100644
index 0000000000..7812dcdab4
Binary files /dev/null and b/branding/icon-app-dark.png differ
diff --git a/branding/icon-app-developer.png b/branding/icon-app-developer.png
new file mode 100644
index 0000000000..c074a3b02b
Binary files /dev/null and b/branding/icon-app-developer.png differ
diff --git a/branding/icon-app-house-dark.png b/branding/icon-app-house-dark.png
new file mode 100644
index 0000000000..32e7f91b3c
Binary files /dev/null and b/branding/icon-app-house-dark.png differ
diff --git a/branding/icon-app-house-light.png b/branding/icon-app-house-light.png
new file mode 100644
index 0000000000..955a7862f0
Binary files /dev/null and b/branding/icon-app-house-light.png differ
diff --git a/branding/icon-app-light.png b/branding/icon-app-light.png
new file mode 100644
index 0000000000..3ea9401bb8
Binary files /dev/null and b/branding/icon-app-light.png differ
diff --git a/branding/snowflake_colored.png b/branding/snowflake_colored.png
new file mode 100644
index 0000000000..f5d9c1d962
Binary files /dev/null and b/branding/snowflake_colored.png differ
diff --git a/branding/snowflake_monochrome.png b/branding/snowflake_monochrome.png
new file mode 100644
index 0000000000..1e0ad19808
Binary files /dev/null and b/branding/snowflake_monochrome.png differ
diff --git a/branding/wintergram-icon.png b/branding/wintergram-icon.png
deleted file mode 100644
index 0cf3146a0a..0000000000
Binary files a/branding/wintergram-icon.png and /dev/null differ
diff --git a/branding/wintergram-icon.svg b/branding/wintergram-icon.svg
deleted file mode 100644
index ef7e7dae2a..0000000000
--- a/branding/wintergram-icon.svg
+++ /dev/null
@@ -1,54931 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py
index 57416703f8..66071f8186 100644
--- a/build-system/Make/Make.py
+++ b/build-system/Make/Make.py
@@ -46,6 +46,8 @@ class BazelCommandLine:
self.show_actions = False
self.enable_sandbox = False
self.disable_provisioning_profiles = False
+ self.disable_extensions = False
+ self.bazel_arguments = []
self.profile_swift = False
self.embed_watch_app = False
self.watch_api_id = None
@@ -138,6 +140,13 @@ class BazelCommandLine:
def set_disable_provisioning_profiles(self):
self.disable_provisioning_profiles = True
+ def set_disable_extensions(self):
+ self.disable_extensions = True
+
+ def add_bazel_arguments(self, arguments):
+ if arguments:
+ self.bazel_arguments.extend(arguments.split())
+
def set_profile_swift(self, value):
self.profile_swift = value
@@ -299,6 +308,11 @@ class BazelCommandLine:
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
+ if self.disable_extensions:
+ combined_arguments += ['--//Telegram:disableExtensions']
+
+ combined_arguments += self.bazel_arguments
+
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
@@ -517,12 +531,21 @@ def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, argum
provisioning_profiles_path=provisioning_path,
additional_codesigning_output_path=additional_codesigning_output_path
)
- if codesigning_data.aps_environment is None:
- print('Could not find a valid aps-environment entitlement in the provided provisioning profiles')
- sys.exit(1)
+ aps_environment = codesigning_data.aps_environment
+ if aps_environment is None:
+ # WinterGram: for unsigned sideload builds (provisioning disabled), the fake profiles
+ # don't match the custom bundle id, so no aps-environment is found. Push won't work
+ # until AltStore / SideStore / LiveContainer re-signs with the user's profile, so we
+ # fall back to a placeholder instead of failing the build.
+ if getattr(arguments, 'disableProvisioningProfiles', False):
+ print('No aps-environment found; defaulting to "development" for unsigned sideload build.')
+ aps_environment = 'development'
+ else:
+ print('Could not find a valid aps-environment entitlement in the provided provisioning profiles')
+ sys.exit(1)
if bazel_command_line is not None:
- build_configuration.write_to_variables_file(bazel_path=bazel_command_line.bazel, use_xcode_managed_codesigning=codesigning_data.use_xcode_managed_codesigning, aps_environment=codesigning_data.aps_environment, path=configuration_repository_path + '/variables.bzl')
+ build_configuration.write_to_variables_file(bazel_path=bazel_command_line.bazel, use_xcode_managed_codesigning=codesigning_data.use_xcode_managed_codesigning, aps_environment=aps_environment, path=configuration_repository_path + '/variables.bzl')
provisioning_profile_files = []
for file_name in os.listdir(provisioning_path):
@@ -578,7 +601,7 @@ def generate_project(bazel, arguments):
generate_dsym = arguments.generateDsym
if arguments.target is not None:
target_name = arguments.target
-
+
call_executable(['killall', 'Xcode'], check_result=False)
xcodeproj_path = generate(
@@ -701,6 +724,13 @@ def build(bazel, arguments):
bazel_command_line.set_split_swiftmodules(arguments.enableParallelSwiftmoduleGeneration)
+ if arguments.disableExtensions:
+ bazel_command_line.set_disable_extensions()
+ if arguments.disableProvisioningProfiles:
+ bazel_command_line.set_disable_provisioning_profiles()
+ if arguments.bazelArguments is not None:
+ bazel_command_line.add_bazel_arguments(arguments.bazelArguments)
+
bazel_command_line.invoke_build()
if arguments.outputBuildArtifactsPath is not None:
@@ -949,7 +979,7 @@ if __name__ == '__main__':
cleanParser = subparsers.add_parser(
'clean', help='''
- Clean local bazel cache. Does not affect files cached remotely (via --cacheHost=...) or
+ Clean local bazel cache. Does not affect files cached remotely (via --cacheHost=...) or
locally in an external directory ('--cacheDir=...')
'''
)
@@ -1043,6 +1073,30 @@ if __name__ == '__main__':
required=True,
help='Build configuration'
)
+ buildParser.add_argument(
+ '--disableExtensions',
+ action='store_true',
+ default=False,
+ help='''
+ Do not build app extensions. Useful for simulator builds or when
+ provisioning profiles for extensions are not available.
+ '''
+ )
+ buildParser.add_argument(
+ '--disableProvisioningProfiles',
+ action='store_true',
+ default=False,
+ help='''
+ Do not use provisioning profiles. Useful for simulator builds or
+ when only fake/self-signed codesigning material is available.
+ '''
+ )
+ buildParser.add_argument(
+ '--bazelArguments',
+ required=False,
+ help='Add additional arguments to the bazel build invocation.',
+ metavar='arguments'
+ )
buildParser.add_argument(
'--enableParallelSwiftmoduleGeneration',
action='store_true',
@@ -1406,7 +1460,7 @@ if __name__ == '__main__':
arguments=args,
additional_codesigning_output_path=remote_input_path
)
-
+
shutil.copyfile(args.configurationPath, remote_input_path + '/configuration.json')
watch_provisioning_profile_remote_path = None
@@ -1448,7 +1502,7 @@ if __name__ == '__main__':
arguments=args,
additional_codesigning_output_path=remote_input_path
)
-
+
shutil.copyfile(args.configurationPath, remote_input_path + '/configuration.json')
TartBuild.remote_build_tart(
diff --git a/build-system/appcenter-configuration.json b/build-system/appcenter-configuration.json
deleted file mode 100755
index b86e141cab..0000000000
--- a/build-system/appcenter-configuration.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "bundle_id": "ph.telegra.Telegraph",
- "api_id": "8",
- "api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb",
- "team_id": "C67CF9S4VU",
- "app_center_id": "4c816ed0-df83-423c-846b-a0a8467dc7d2",
- "is_internal_build": "false",
- "is_appstore_build": "true",
- "appstore_id": "686449807",
- "app_specific_url_scheme": "tg",
- "premium_iap_product_id": "org.telegram.telegramPremium.monthly",
- "enable_siri": true,
- "enable_icloud": true
-}
\ No newline at end of file
diff --git a/build-system/wintergram-development-configuration.example.json b/build-system/wintergram-development-configuration.example.json
new file mode 100644
index 0000000000..6c33309cc8
--- /dev/null
+++ b/build-system/wintergram-development-configuration.example.json
@@ -0,0 +1,14 @@
+{
+ "bundle_id": "dev.reekeer.wintergram",
+ "api_id": "2040",
+ "api_hash": "b18441a1ff607e10a989891a5462e627",
+ "team_id": "0000000000",
+ "app_center_id": "0",
+ "is_internal_build": "true",
+ "is_appstore_build": "false",
+ "appstore_id": "0",
+ "app_specific_url_scheme": "wnt",
+ "premium_iap_product_id": "",
+ "enable_siri": false,
+ "enable_icloud": false
+}
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
new file mode 100644
index 0000000000..5edba15da1
--- /dev/null
+++ b/docs/FEATURES.md
@@ -0,0 +1,153 @@
+# WinterGram Features
+
+This document describes every user-facing WinterGram capability. Settings are grouped under the **WinterGram** tab in the app (Settings → WinterGram).
+
+---
+
+## Ghost Mode
+
+Control what activity signals you send to Telegram servers.
+
+| Feature | Description |
+| :-- | :-- |
+| Ghost Mode presets | Off, Messages only, Stories only, or Everything |
+| Send without sound | Never, only in Ghost Mode, or always |
+| Scheduled send trick | Route ghost-mode sends through the scheduled path so the chat stays unread |
+| Track online status | Local log of when contacts go online (Online Tracker screen) |
+| Story ghost | View stories without marking them seen |
+| Mark read after action | Mark chats read locally after you reply or react, without sending receipts |
+| Go offline after online | Send a one-shot offline presence after appearing online |
+
+---
+
+## History and Recovery
+
+Keep message content on this device even when it is removed or changed on the server.
+
+| Feature | Description |
+| :-- | :-- |
+| Save deleted messages | Retain messages after the other side deletes them |
+| Save edit history | Store every revision of edited messages |
+| Save from bots | Include bot messages in the deleted-message cache |
+| Save self-destruct | Keep disappearing messages before they vanish |
+| Dim deleted messages | Render saved deletions at reduced opacity |
+| Clear saved deletions | Remove all locally cached deleted messages |
+| Deleted messages browser | Data & Storage → Deleted Messages: pie chart by type, per-category cleanup, **top chats** by deletion count |
+
+---
+
+## Hidden Archive
+
+A separate stash for chats that should not appear in the main list.
+
+| Feature | Description |
+| :-- | :-- |
+| Stash chats | Move chats to a settings-only archive via context menu |
+| Stashed chats list | Browse and unstash from WinterGram → Core → Hidden Archive |
+| Mute notifications | Drop in-app banners for stashed peers |
+| Auto mark as read | Mark incoming stashed messages read locally |
+| Passcode | Optional passcode gate before opening the stash |
+
+---
+
+## Features (anti-features and conveniences)
+
+| Feature | Description |
+| :-- | :-- |
+| Disable ads | Hide sponsored messages in channels |
+| Local Premium | Unlock Premium-gated UI locally (no server-side subscription) |
+| Hide stories | Remove stories from the chat list header |
+| Hide Premium statuses | Conceal Premium badges and statuses |
+| Disable open-link warning | Skip the concealed-URL confirmation dialog |
+| Allow saving restricted content | Bypass copy-protection on protected media |
+| Allow screenshots everywhere | Disable screenshot blocking in supported views |
+| Confirm stickers / GIFs / voice | Ask before sending each media type |
+
+---
+
+## Chat
+
+| Feature | Description |
+| :-- | :-- |
+| Show message seconds | Display seconds in message timestamps |
+| Show peer ID | Hidden, Telegram API form, or Bot API form in profiles |
+| Show registration date | Display account creation date where available |
+| Hide edited mark | Suppress the "edited" label on messages |
+| Message translation | Context-menu translation with selectable provider |
+| Translation provider | Telegram, Google, Yandex, or system |
+| Increase WebView height | Taller viewport for mini-apps |
+| Only added emoji and stickers | Limit picker to your custom sets |
+| Forward without author | Omit original author on forwards |
+| Default reaction | Custom emoji reaction used by default |
+
+---
+
+## Appearance
+
+| Feature | Description |
+| :-- | :-- |
+| Material Design | Material-styled switches and controls |
+| Single corner radius | Apply bubble radius to one corner only |
+| Avatar shape | Slider from square through squircle to round |
+| Bubble radius | Adjustable message bubble corner radius |
+| Custom font | Override the UI typeface |
+| Monospace font | Override the code/monospace typeface |
+| Icon pack | WinterGram, Ayu, exteraGram, or Telegram alternate icons |
+| Liquid Glass | Frosted translucent surfaces with per-area toggles |
+| Liquid Glass vibrancy | Extra vibrancy on glass layers |
+| Apply to chat list / nav bars / tab bar / bubbles | Per-surface glass control |
+
+---
+
+## Spoofing
+
+Report different device metadata to Telegram and mini-apps. Changing API credentials requires re-login.
+
+| Feature | Description |
+| :-- | :-- |
+| Spoof device model | Override the hardware identifier |
+| Spoof app version | Override the client version string |
+| WebView platform | Automatic, iOS, Android, macOS, or desktop |
+| Custom API ID / Hash | Use your own credentials from my.telegram.org |
+| Spoof presets | Save and recall device/version profiles |
+
+---
+
+## Badges and Branding
+
+| Feature | Description |
+| :-- | :-- |
+| WinterGram badge | Composed at runtime from white backplate + snowflake shapes, tinted to the theme |
+| Developer badge | Backplated snowflake for contributors |
+| Official channel badge | Snowflake for official WinterGram resources |
+| Use default branding | Restore standard Telegram naming in the UI |
+
+---
+
+## Accounts and Data
+
+| Feature | Description |
+| :-- | :-- |
+| Multiple accounts | Up to 100 accounts on one device |
+| Deep links | `wnt://` scheme mirrors `tg://`; `wnt://wintergram/` opens a settings category |
+| Local-only storage | Deleted and edited history never leaves the device |
+| iCloud backup | Account data is included in device backups |
+
+---
+
+## Localization
+
+| Language | Coverage |
+| :-- | :-- |
+| English | All WinterGram settings strings |
+| Russian | Full WinterGram UI via bundled seed translations |
+
+---
+
+## Channels and Links
+
+| Resource | URL |
+| :-- | :-- |
+| Channel | https://t.me/wntgram |
+| Beta | https://t.me/wntbeta |
+| GitHub | https://github.com/reekeer/WinterGram |
diff --git a/docs/wintergram-features.md b/docs/wintergram-features.md
index dcbe4aff27..ffeb2539e8 100644
--- a/docs/wintergram-features.md
+++ b/docs/wintergram-features.md
@@ -1,86 +1,111 @@
# WinterGram — feature → implementation map
-This document maps every WinterGram option to where it is (or needs to be) wired into the
-Telegram-iOS codebase. The settings themselves live in a single store:
+> Developer reference. User-facing overview: [`docs/FEATURES.md`](FEATURES.md).
+
+This document maps every WinterGram option to where it is wired into the
+codebase. The settings themselves live in a single store:
- `submodules/TelegramUIPreferences/Sources/WinterGramSettings.swift` — the `WinterGramSettings`
`Codable` struct, its sub-structs (`WinterGramLiquidGlass`) and enums, plus
- `updateWinterGramSettingsInteractively(...)` and `winterGramSettings(accountManager:)`.
+ `updateWinterGramSettingsInteractively(...)`, `winterGramSettings(accountManager:)`, and the
+ synchronous snapshot `currentWinterGramSettings` (kept fresh by
+ `observeWinterGramSettings(accountManager:)`, started from `AppDelegate`).
- `submodules/TelegramUIPreferences/Sources/PostboxKeys.swift` — the shared-data key
`ApplicationSpecificSharedDataKeys.winterGramSettings` (value `23`).
+- `submodules/TelegramCore/Sources/WinterGram/WinterGramCoreSettings.swift` — a minimal
+ mirror for hooks inside `TelegramCore` (which cannot import `TelegramUIPreferences`);
+ fed by the same observer.
Read the store anywhere that already has an `AccountContext` / `AccountManager` via
-`winterGramSettings(accountManager:)`, and write it via `updateWinterGramSettingsInteractively`.
+`winterGramSettings(accountManager:)` (reactive) or `currentWinterGramSettings` (sync),
+and write it via `updateWinterGramSettingsInteractively`.
## Status legend
-- **Store** — the setting exists and persists (done in this repo).
-- **Hook** — the place in the app where behavior must read the setting.
+- ✅ — setting persists and the behavior hook is in place.
+- ⏳ — setting persists, behavior not fully hooked yet.
## Privacy & Ghost Mode
-| Setting | Hook |
-| :-- | :-- |
-| `ghostModeEnabled` | Master switch read by the read-receipt / online-status / typing senders below. |
-| `sendReadReceipts` | `TelegramCore` history read — gate `_internal_applyMaxReadIndex` / outgoing `messages.readHistory` calls. |
-| `sendReadStories` | Story view reporting — gate `markStoryAsSeen` network calls. |
-| `sendOnlineStatus` | Online presence — gate `account.updatePresence` / `updateStatus`. |
-| `sendUploadProgress` | Typing/upload activity — gate `ChatActivity` / `setTyping` reporting. |
-| `sendOfflineAfterOnline` | Emit a one-shot offline presence packet after the app goes online. |
-| `markReadAfterAction` | After replying/reacting, locally mark the chat read without sending receipts. |
-| `useScheduledMessages` | "Отложка" — when ghosting, send via the scheduled-messages path. |
-| `sendWithoutSound` | Outgoing message flags — set `silent` per `shouldSendWithoutSound`. |
-| `suggestGhostBeforeStory` | Story viewer — present the ghost confirmation before opening. |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `ghostModeEnabled` | ✅ | Master switch read by the gates below. |
+| `sendReadReceipts` | ✅ | `AccountContext.applyMaxReadIndex` (`submodules/TelegramUI/Sources/AccountContext.swift`) returns early in ghost mode. |
+| `sendReadStories` | ✅ | All four `markAsSeen` implementations in `StoryChatContent.swift` return early in ghost mode. |
+| `sendOnlineStatus` | ✅ | `SharedWakeupManager` forces `shouldKeepOnlinePresence` to false in ghost mode. |
+| `sendUploadProgress` | ✅ | Typing-activity subscription in `ChatController.swift` skips `updateLocalInputActivity` in ghost mode. |
+| `sendOfflineAfterOnline` | ⏳ | Emit a one-shot offline presence packet after going online. |
+| `markReadAfterAction` | ⏳ | After replying/reacting, locally mark read without sending receipts. |
+| `useScheduledMessages` | ✅ | "Отложка": `transformEnqueueMessages` in `ChatController.swift` routes ghost-mode sends through the scheduled path (now + 12 s) so sending doesn't mark the chat read. |
+| `sendWithoutSound` | ✅ | `transformEnqueueMessages` computes `effectiveSilentPosting` from never / in-ghost / always. |
+| `suggestGhostBeforeStory` | ⏳ | Story viewer — present the ghost confirmation before opening. |
## History & Recovery
-| Setting | Hook |
-| :-- | :-- |
-| `saveDeletedMessages` | Hook the deletion path in `Postbox` history removal; mirror messages into a local store before they are purged. |
-| `saveMessagesHistory` | On `EditMessage` updates, append the previous version to a local edit-history store. |
-| `semiTransparentDeletedMessages` | `ChatMessageItemView` — render saved-deleted bubbles at reduced alpha. |
-| `deletedMark` / `editedMark` | Message footer rendering in the bubble content nodes. |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `saveDeletedMessages` | ✅ | Remote deletions (`.DeleteMessages` / `.DeleteMessagesWithGlobalIds`) skipped in `AccountStateManagementUtils.swift` via `currentWinterGramCoreSettings`. |
+| `saveMessagesHistory` | ✅ | On remote `.EditMessage`, the previous text/entities/timestamp are appended to `WinterGramEditHistoryAttribute` (`submodules/TelegramCore/Sources/WinterGram/`); registered in `AccountManager.swift`. Viewing UI: ⏳. |
+| `semiTransparentDeletedMessages` | ⏳ | Render kept-deleted bubbles at reduced alpha. |
-## Hidden Archive ("AАrchive")
+## Hidden Archive ("ААрхив")
-| Setting | Hook |
-| :-- | :-- |
-| `stashedPeerIds` | Filter the chat list to hide these peers from the main list; show them only in the dedicated WinterGram archive screen. |
-| `stashMuteNotifications` | Notification service extension — suppress notifications for stashed peers. |
-| `stashAutoMarkRead` | On receiving from a stashed peer, locally mark read (respecting Ghost Mode). |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `stashedPeerIds` | ✅ | Hidden from the main tab in `ChatListNodeEntries.swift`; stash/unstash via chat-list context menu (`ChatContextMenus.swift`); browse via Settings → WinterGram → Stashed Chats (`WinterGramStashController.swift`). |
+| `stashMuteNotifications` | ✅ | In-app notification pipeline in `ApplicationContext.swift` drops banners for stashed peers. (APNs pushes need the NotificationService extension: ⏳.) |
+| `stashAutoMarkRead` | ✅ | Same pipeline calls `applyMaxReadIndexInteractively` for stashed peers. |
## Anti-Features
-| Setting | Hook |
-| :-- | :-- |
-| `disableAds` | Sponsored-messages fetch in `TelegramCore` (`getAdMessages`) — return empty when disabled. |
-| `localPremium` | `isPremium` resolution in the UI layer — treat as Premium locally for gated UI. |
-| `shadowBanIds` | Chat history filtering — drop incoming messages from these peers from the rendered list. |
-| `disableStories` | Story list assembly — hide the stories strip. |
-| `hidePremiumStatuses` | Peer title rendering — drop Premium/emoji-status badges. |
-| `disableOpenLinkWarning` | URL open path — skip the "open this link?" confirmation. |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `disableAds` | ✅ | Ad insertion gate in `ChatHistoryEntriesForView.swift`. |
+| `localPremium` | ✅ | `isPremium` resolution in `submodules/TelegramUI/Sources/AccountContext.swift`. |
+| `shadowBanIds` | ✅ | Entry filter by author in `ChatHistoryEntriesForView.swift`. |
+| `disableStories` | ✅ | `shouldDisplayStoriesInChatListHeader` in `ChatListControllerNode.swift` returns false. |
+| `hidePremiumStatuses` | ✅ | `ChatTitleView` / `ChatTitleComponent` / `ChatListItem` / `ItemListPeerItem`. |
+| `disableOpenLinkWarning` | ✅ | Concealed-link alert gate in `OpenUserGeneratedUrl.swift`. |
+
+## In-app purchases
+
+Fully disabled: `InAppPurchaseManager.buyProduct` fails immediately with `.cantMakePayments`
+(every purchase screen maps that to a localized error), and `PremiumIntroScreen.buy()` shows a
+"subscribe via the official Telegram app" alert before reaching the manager. Redeeming gift
+codes still works (not an IAP).
## Chat Conveniences
-| Setting | Hook |
-| :-- | :-- |
-| `stickerConfirmation` / `gifConfirmation` / `voiceConfirmation` | Send paths in the chat input panel — present a confirm alert before sending. |
-| `showMessageSeconds` | Timestamp formatting in the bubble footer. |
-| `showPeerId` | Peer info / chat title — append the ID in Telegram or Bot API form. |
-| `translateMessages` / `translationProvider` | Message context menu translate action + provider selection. |
-| `webviewSpoofPlatform` / `increaseWebviewHeight` | WebApp controller — set the spoofed `tg_platform` / viewport height. |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `stickerConfirmation` / `gifConfirmation` | ✅ | `ChatController.swift` send paths. |
+| `voiceConfirmation` | ✅ | `ChatControllerMediaRecording.swift`. |
+| `showMessageSeconds` | ✅ | `StringForMessageTimestampStatus.swift`. |
+| `showPeerId` | ✅ | ID row (long-press copies) in `PeerInfoProfileItems.swift` for users and channels/groups, honoring Telegram/Bot-API format. |
+| `translateMessages` / `translationProvider` | ⏳ | Message context-menu translate + provider. |
+| `webviewSpoofPlatform` | ✅ | `BotWebView.swift` in TelegramCore reads `currentWinterGramCoreSettings.webviewPlatform` (ios / android / macos / tdesktop), fed by the settings observer. |
+| `increaseWebviewHeight` | ⏳ | WebApp controller viewport. |
## Appearance & Customization
-| Setting | Hook |
-| :-- | :-- |
-| `liquidGlass.*` | `Display` blur/material layers behind the chat list, nav bar, and tab bar; read `enabled`, `transparency`, `blurRadius`, `tintColor`, per-surface flags. |
-| `materialDesign` | Switch/control styling in `ItemListUI` components. |
-| `avatarCornerRadius` / `singleCornerRadius` | Avatar node corner rounding in `AvatarNode`. |
-| `messageBubbleRadius` / `removeMessageTail` | Bubble background drawing in the chat message backgrounds. |
-| `customFont` / `monoFont` | `PresentationData` font resolution. |
-| `appIcon` / `iconPack` | Alternate-icon switching via `UIApplication.setAlternateIconName`; see `Telegram/Telegram-iOS/DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset`. |
-| `showOnlyAddedEmojisAndStickers` | Emoji/sticker panel data sources — filter to installed packs. |
+| Setting | Status | Hook |
+| :-- | :-- | :-- |
+| `liquidGlass.*` | ⏳ | Blur/material layers behind chat list, nav bar, tab bar. |
+| `materialDesign` | ⏳ | Switch/control styling. |
+| `avatarCornerRadius` / `singleCornerRadius` | ⏳ | `AvatarNode` corner rounding. Note: photos are circle-clipped inside the bitmap (`PeerAvatar.swift` `roundCorners` mask), so a real implementation must touch every render path, not just `imageNode.cornerRadius`. |
+| `messageBubbleRadius` / `removeMessageTail` | ⏳ | Bubble background drawing. |
+| `customFont` / `monoFont` | ⏳ | `PresentationData` font resolution. |
+| `appIcon` / `iconPack` | ⏳ | Alternate-icon switching; assets in `DefaultAppIcon.xcassets/WinterGramDarkIcon.appiconset`. |
+| `showOnlyAddedEmojisAndStickers` | ⏳ | Emoji/sticker panel data sources. |
+
+## Accounts
+
+- Unlimited accounts: `maximumNumberOfAccounts` / `maximumPremiumNumberOfAccounts` = 100 in
+ `submodules/AccountUtils/Sources/AccountUtils.swift`; the add-account flow in
+ `PeerInfoScreenSettingsActions.swift` uses the same constants.
+- Account data is included in iCloud/iTunes device backups (`isExcludedFromBackup = false` in
+ `TelegramCore/Sources/Account/AccountManager.swift`). Tradeoff: session auth keys become part
+ of the backup.
## Deep links — `wnt://`
@@ -90,7 +115,7 @@ Normalized to `tg://` at the app entry by `normalizeWinterGramUrlScheme(_:)` in
## Settings UI
-A dedicated **WinterGram** entry should be added to the settings list
-(`submodules/SettingsUI` / the PeerInfo settings screen) that opens an `ItemListController`
-backed by `winterGramSettings(accountManager:)` and writing through
-`updateWinterGramSettingsInteractively`. Group the rows by the sections above.
+The **WinterGram** entry is the first row of Settings (snowball icon,
+`PresentationResourcesSettings.winterGram`), opening
+`submodules/SettingsUI/Sources/WinterGramSettingsController.swift`. The Hidden Archive browser
+lives in `WinterGramStashController.swift` next to it.
diff --git a/docs/wintergram-setup.md b/docs/wintergram-setup.md
new file mode 100644
index 0000000000..3c0b2e6785
--- /dev/null
+++ b/docs/wintergram-setup.md
@@ -0,0 +1,176 @@
+# WinterGram Build & Install Guide
+
+This guide covers how to build WinterGram from source, run it in the iOS Simulator, install it on a physical iPhone without a paid Apple Developer Program, and sideload the resulting IPA.
+
+## Requirements
+
+- macOS with Xcode (the project is tested on Xcode 26.x)
+- Python 3
+- ~60 GB of free disk space (Bazel cache + build artifacts)
+- A free Apple ID (for on-device installation)
+
+## 1. Clone the repository
+
+```sh
+git clone --recursive https://github.com/reekeer/WinterGram.git
+cd WinterGram
+```
+
+If you cloned without `--recursive`:
+
+```sh
+git submodule update --init --recursive
+```
+
+## 2. Configure build credentials
+
+WinterGram needs a development configuration with your own credentials.
+
+1. Copy the example file:
+
+ ```sh
+ cp build-system/wintergram-development-configuration.example.json \
+ build-system/wintergram-development-configuration.json
+ ```
+
+2. Edit `build-system/wintergram-development-configuration.json`:
+ - `bundle_id` — a unique bundle identifier (e.g. `dev.you.wintergram`).
+ - `api_id` / `api_hash` — obtain from .
+ - `team_id` — your Apple Developer Team ID (a 10-character string). For a free Apple ID, create an Apple Development certificate in Xcode → Settings → Accounts, then find the Team ID in Keychain Access under the certificate's **Organizational Unit** field.
+ - `app_specific_url_scheme` is `wnt` by default.
+
+> **Important:** `api_id` and `api_hash` are baked into the app at build time. You cannot change them after installation because your Telegram session is tied to the `api_id` used when logging in.
+
+The committed example uses public test values (`api_id: 2040`). Replace them with your own before installing on a personal device.
+
+## 3. Build an unsigned IPA
+
+### Convenience wrapper (recommended)
+
+`scripts/build-wintergram.sh` wraps the Bazel/Make.py invocations and emits WinterGram-named IPAs in `build/`:
+
+```sh
+./scripts/build-wintergram.sh sim # simulator -> build/WinterGram-Simulator.ipa
+./scripts/build-wintergram.sh --install # build sim + install into the booted Simulator (sim only)
+./scripts/build-wintergram.sh --install --run # also launch it
+./scripts/build-wintergram.sh sideload # device IPA -> build/WinterGram.ipa
+./scripts/build-wintergram.sh livecontainer # unsigned IPA -> build/WinterGram-LiveContainer.ipa
+./scripts/build-wintergram.sh all # all of the above
+```
+
+`--clean` wipes `build/` first; `--open-build-dir` reveals the output in Finder; `--help` lists everything.
+
+The raw commands below are equivalent if you prefer to invoke Make.py directly.
+
+The fastest path to a device IPA is a terminal build:
+
+```sh
+python3 build-system/Make/Make.py --overrideXcodeVersion \
+ --cacheDir ~/telegram-bazel-cache \
+ build \
+ --configurationPath build-system/wintergram-development-configuration.json \
+ --xcodeManagedCodesigning \
+ --buildNumber=1 \
+ --configuration=debug_arm64
+```
+
+The finished `.ipa` is written to `bazel-bin/Telegram/`.
+
+For a simulator build, use `debug_sim_arm64` and fake codesigning files (the Simulator does not validate signatures, but Bazel still requires provisioning profiles):
+
+```sh
+python3 build-system/Make/Make.py --overrideXcodeVersion \
+ --cacheDir ~/telegram-bazel-cache \
+ build \
+ --configurationPath build-system/wintergram-development-configuration.json \
+ --codesigningInformationPath build-system/fake-codesigning \
+ --buildNumber=1 \
+ --configuration=debug_sim_arm64
+```
+
+Add `--continueOnError` after `build` to see every error in one pass. `--xcodeManagedCodesigning` is intended for `generateProject`; for terminal builds with extensions, use `--codesigningInformationPath build-system/fake-codesigning`.
+
+## 4. Install on a physical iPhone (free Apple ID)
+
+### With AltStore / SideStore
+
+1. Build a device IPA (`debug_arm64` or `release_arm64`) with your Team ID in the config.
+2. Install AltStore on your phone (via AltServer on your Mac).
+3. In AltStore, tap **+**, select the built `.ipa`, and let AltStore re-sign and install it.
+4. A free Apple ID must re-sign the app every 7 days. AltStore and SideStore can refresh this automatically in the background.
+
+### With Xcode directly
+
+1. Generate the Xcode project (see below).
+2. Select the **Telegram** scheme and your connected iPhone as the destination.
+3. In the target's **Signing & Capabilities**, choose your Personal Team.
+4. Press ⌘R. The first time, trust the developer on the device in Settings → General → VPN & Device Management.
+
+Free provisioning profiles last 7 days and are limited to 3 apps per Apple ID. Push notifications via APNs, Siri, and iCloud are unavailable with a free account, so the dev config has `enable_siri` and `enable_icloud` disabled.
+
+## 5. Generate an Xcode project
+
+```sh
+python3 build-system/Make/Make.py --overrideXcodeVersion \
+ --cacheDir ~/telegram-bazel-cache \
+ generateProject \
+ --configurationPath build-system/wintergram-development-configuration.json \
+ --xcodeManagedCodesigning
+```
+
+To generate a simulator-only project without provisioning profiles, add `--disableProvisioningProfiles`.
+
+The command opens the generated `Telegram.xcodeproj` automatically. If it does not, the project is in the repository root.
+
+## 6. Run in the Simulator
+
+1. Open the generated `Telegram.xcodeproj`.
+2. Select the **Telegram** scheme and any iPhone Simulator.
+3. Press ⌘R.
+
+The first build is slow (40–90 minutes) because WebRTC and ffmpeg are compiled from source. Incremental builds are much faster. If Xcode hangs on "build-request.json not updated yet", cancel and run again; this is a known rules_xcodeproj quirk.
+
+## App icons
+
+All WinterGram app-icon assets are generated from source PNGs in `branding/` by a single script:
+
+```sh
+./scripts/generate-app-icons.sh
+```
+
+Each source named `branding/wnt-app-icon-.png` (square, ≥1024×1024) becomes an alternate
+icon `WinterGram`. The two shipping icons are `wnt-app-icon-dark.png` (also the primary
+home-screen icon) and `wnt-app-icon-light.png`. The script regenerates the `.alticon` folders, the
+primary `WinterGramDarkIcon.appiconset`, and the in-app preview imagesets in one pass.
+
+A brand-new variant is generated but must still be registered manually — the script prints the three
+spots: `Telegram/BUILD` (`alternate_icon_folders`), `AppDelegate.getAvailableAlternateIcons()`, and the
+preview-imageset mapping in `applicationBindings`.
+
+## Repository layout
+
+```
+WinterGram/
+├── Telegram/ ← App entry point and extensions
+│ ├── Telegram-iOS/ ← Info.plist, assets, xcconfig
+│ ├── Share / SiriIntents / NotificationService / ...
+│ └── WatchApp/ ← watchOS client snapshot
+├── submodules/ ← Functionality split into Bazel modules
+│ ├── TelegramUIPreferences/Sources/WinterGramSettings.swift
+│ └── SettingsUI/Sources/WinterGramSettingsController.swift
+├── branding/ ← Source art: app-icon PNGs (wnt-app-icon-*.png) + badge/snowflake shapes
+├── scripts/ ← Build + tooling
+│ ├── build-wintergram.sh ← Convenience build wrapper (sim / sideload / livecontainer)
+│ └── generate-app-icons.sh ← App-icon generator (reads branding/wnt-app-icon-*.png)
+├── docs/ ← Documentation
+├── build-system/ ← Bazel wrapper and configs
+│ └── wintergram-development-configuration.json ← your dev config
+├── third-party/ ← External dependencies
+└── Tests/ ← Bazel test app targets
+```
+
+## Troubleshooting
+
+- **"No .mobileprovision targets for extensions"** when using `--xcodeManagedCodesigning` in a terminal build: use `--codesigningInformationPath build-system/fake-codesigning` instead, or use `generateProject` for Xcode-managed signing.
+- **Build fails on the first try:** ensure submodules are fully initialized and you have enough disk space.
+- **App crashes on launch after re-signing:** make sure the `bundle_id` in your config is unique and the `team_id` matches the certificate used to sign.
diff --git a/scripts/build-wintergram.sh b/scripts/build-wintergram.sh
new file mode 100755
index 0000000000..18f1f1cf2b
--- /dev/null
+++ b/scripts/build-wintergram.sh
@@ -0,0 +1,300 @@
+#!/bin/bash
+# WinterGram build wrapper — produces WinterGram-named IPAs for every install target.
+# Lives in scripts/; all paths are resolved relative to the repo root (the script cd's there).
+#
+# Usage:
+# ./scripts/build-wintergram.sh
+# ./scripts/build-wintergram.sh all
+# ./scripts/build-wintergram.sh sideload
+# ./scripts/build-wintergram.sh livecontainer
+# ./scripts/build-wintergram.sh sim
+#
+# Convenience:
+# ./scripts/build-wintergram.sh --install # build the simulator IPA and install it into the active Simulator (sim mode only)
+# ./scripts/build-wintergram.sh --install --run # also launch the app in the active Simulator
+# ./scripts/build-wintergram.sh --clean # remove ./build before building
+# ./scripts/build-wintergram.sh --open-build-dir # open ./build in Finder after build
+# ./scripts/build-wintergram.sh --help
+
+set -euo pipefail
+# Resolve to the repo root regardless of where the script is invoked from (it lives in scripts/).
+cd "$(dirname "$0")/.."
+REPO="$(pwd)"
+source ~/.zshrc 2>/dev/null || true
+
+OUT_DIR="build"
+SIDELOAD_NAME="WinterGram.ipa"
+LC_NAME="WinterGram-LiveContainer.ipa"
+SIM_NAME="WinterGram-Simulator.ipa"
+WNT_BUNDLE_ID="dev.reekeer.wintergram"
+BAZEL="./build-input/bazel-8.4.2-darwin-arm64"
+DEVICE_SRC="bazel-bin/Telegram/Telegram.ipa"
+
+MODE="all"
+INSTALL_SIM=0
+RUN_SIM=0
+CLEAN=0
+OPEN_BUILD_DIR=0
+
+usage() {
+ cat < build/$SIDELOAD_NAME
+ livecontainer, lc Build unsigned LiveContainer IPA -> build/$LC_NAME
+ sim, simulator Build simulator IPA -> build/$SIM_NAME
+
+Options:
+ --install Build the simulator IPA and install it into the active booted Simulator.
+ This forces "sim" mode (install only makes sense for the simulator).
+ --run Launch the app after --install (implies --install).
+ --clean Remove ./build before building.
+ --open-build-dir Open ./build in Finder after build.
+ -h, --help Show this help.
+
+Examples:
+ $0 --install
+ $0 --install --run
+ $0 sideload --clean
+EOF
+}
+
+die() {
+ echo "ERROR: $*" >&2
+ exit 1
+}
+
+ipa_size() {
+ du -h "$1" | cut -f1
+}
+
+require_cmd() {
+ command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
+}
+
+# --- args ------------------------------------------------------------------
+
+MODE_WAS_EXPLICIT=0
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ all|sideload|device|livecontainer|lc|sim|simulator)
+ MODE="$1"
+ MODE_WAS_EXPLICIT=1
+ ;;
+ --install)
+ INSTALL_SIM=1
+ ;;
+ --run)
+ RUN_SIM=1
+ INSTALL_SIM=1
+ ;;
+ --clean)
+ CLEAN=1
+ ;;
+ --open-build-dir)
+ OPEN_BUILD_DIR=1
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ die "unknown argument: $1"
+ ;;
+ esac
+ shift
+done
+
+# --install is simulator-only: installing a device/livecontainer IPA into a Simulator makes no
+# sense, so --install always forces "sim" mode (warning if a conflicting mode was given).
+if [ "$INSTALL_SIM" -eq 1 ]; then
+ if [ "$MODE_WAS_EXPLICIT" -eq 1 ] && [ "$MODE" != "sim" ] && [ "$MODE" != "simulator" ]; then
+ echo "==> --install is simulator-only; ignoring mode '$MODE' and building 'sim'." >&2
+ fi
+ MODE="sim"
+fi
+
+if [ "$CLEAN" -eq 1 ]; then
+ echo "==> Cleaning ./$OUT_DIR ..."
+ rm -rf "$OUT_DIR"
+fi
+
+mkdir -p "$OUT_DIR"
+
+# --- simulator -------------------------------------------------------------
+
+build_sim() {
+ echo "==> [Simulator] build (debug_sim_arm64) ..."
+ python3 build-system/Make/Make.py --overrideXcodeVersion \
+ --cacheDir "$HOME/telegram-bazel-cache" \
+ build \
+ --configurationPath build-system/wintergram-development-configuration.json \
+ --codesigningInformationPath build-system/fake-codesigning-wintergram \
+ --disableExtensions \
+ --buildNumber=1 --configuration=debug_sim_arm64
+
+ [ -f "$DEVICE_SRC" ] || die "simulator artifact not found at $DEVICE_SRC"
+
+ cp -f "$DEVICE_SRC" "$OUT_DIR/$SIM_NAME"
+ echo "==> [Simulator] done: $OUT_DIR/$SIM_NAME ($(ipa_size "$OUT_DIR/$SIM_NAME"))"
+}
+
+ensure_booted_simulator() {
+ require_cmd xcrun
+
+ if ! xcrun simctl list devices booted | grep -q "(Booted)"; then
+ die "no active booted Simulator found. Open Simulator.app and boot a device first."
+ fi
+}
+
+install_sim() {
+ ensure_booted_simulator
+
+ local IPA="$OUT_DIR/$SIM_NAME"
+ [ -f "$IPA" ] || die "simulator IPA not found at $IPA"
+
+ echo "==> [Simulator] installing into active booted Simulator ..."
+
+ local TMP_DIR
+ TMP_DIR="$(mktemp -d)"
+
+ unzip -q "$IPA" -d "$TMP_DIR"
+
+ local APP_PATH
+ APP_PATH="$(find "$TMP_DIR/Payload" -maxdepth 1 -type d -name "*.app" | head -n 1)"
+
+ [ -n "${APP_PATH:-}" ] || {
+ rm -rf "$TMP_DIR"
+ die "no .app found inside $IPA"
+ }
+
+ xcrun simctl install booted "$APP_PATH"
+
+ local INSTALLED_BUNDLE_ID
+ INSTALLED_BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null || true)"
+
+ echo "==> [Simulator] installed: $(basename "$APP_PATH")"
+
+ if [ "$RUN_SIM" -eq 1 ]; then
+ [ -n "$INSTALLED_BUNDLE_ID" ] || {
+ rm -rf "$TMP_DIR"
+ die "could not read CFBundleIdentifier from app Info.plist"
+ }
+
+ echo "==> [Simulator] launching $INSTALLED_BUNDLE_ID ..."
+ xcrun simctl launch booted "$INSTALLED_BUNDLE_ID" || true
+ fi
+
+ rm -rf "$TMP_DIR"
+}
+
+# --- device build shared by sideload + livecontainer -----------------------
+
+ensure_cert() {
+ # Device builds need a codesigning identity in the keychain, even a throwaway one.
+ if ! security find-certificate -c "Apple Distribution: Telegram FZ-LLC (C67CF9S4VU)" >/dev/null 2>&1; then
+ echo "==> Importing throwaway signing cert into login keychain ..."
+ security import build-system/fake-codesigning/certs/SelfSigned.p12 -P "" -A >/dev/null 2>&1 || true
+ fi
+}
+
+build_device() {
+ ensure_cert
+
+ echo "==> [Device] build (debug_arm64, ${WNT_BUNDLE_ID}, extensions disabled) ..."
+ python3 build-system/Make/Make.py --overrideXcodeVersion \
+ --cacheDir "$HOME/telegram-bazel-cache" \
+ build \
+ --configurationPath build-system/wintergram-development-configuration.json \
+ --codesigningInformationPath build-system/fake-codesigning-wintergram \
+ --disableExtensions \
+ --buildNumber=1 --configuration=debug_arm64
+
+ [ -f "$DEVICE_SRC" ] || die "device artifact not found at $DEVICE_SRC"
+}
+
+make_sideload() {
+ cp -f "$DEVICE_SRC" "$OUT_DIR/$SIDELOAD_NAME"
+ echo "==> [Sideload] done: $OUT_DIR/$SIDELOAD_NAME ($(ipa_size "$OUT_DIR/$SIDELOAD_NAME"))"
+}
+
+make_livecontainer() {
+ echo "==> [LiveContainer] repackaging unsigned ..."
+
+ local TMP_DIR
+ TMP_DIR="$(mktemp -d)"
+
+ unzip -q "$DEVICE_SRC" -d "$TMP_DIR"
+
+ find "$TMP_DIR/Payload/Telegram.app" -type f \( -name Telegram -o -name "*.dylib" \) -print0 2>/dev/null \
+ | xargs -0 codesign --remove-signature 2>/dev/null || true
+
+ find "$TMP_DIR/Payload/Telegram.app" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
+ local bin
+ bin="$fw/$(basename "$fw" .framework)"
+ [ -f "$bin" ] && codesign --remove-signature "$bin" 2>/dev/null || true
+ done
+
+ find "$TMP_DIR" -type d \( -name _CodeSignature -o -name SC_Info \) -exec rm -rf {} + 2>/dev/null || true
+ find "$TMP_DIR/Payload/Telegram.app" -name "embedded.mobileprovision" -delete 2>/dev/null || true
+
+ rm -f "$OUT_DIR/$LC_NAME"
+
+ local ZIP_ITEMS="Payload"
+ [ -d "$TMP_DIR/SwiftSupport" ] && ZIP_ITEMS="$ZIP_ITEMS SwiftSupport"
+
+ (
+ cd "$TMP_DIR"
+ zip -qr "$REPO/$OUT_DIR/$LC_NAME" $ZIP_ITEMS
+ )
+
+ rm -rf "$TMP_DIR"
+
+ echo "==> [LiveContainer] done: $OUT_DIR/$LC_NAME ($(ipa_size "$OUT_DIR/$LC_NAME"))"
+}
+
+# --- main ------------------------------------------------------------------
+
+case "$MODE" in
+ sim|simulator)
+ build_sim
+ ;;
+ sideload|device)
+ build_device
+ make_sideload
+ ;;
+ livecontainer|lc)
+ build_device
+ make_livecontainer
+ ;;
+ all)
+ # One device build feeds BOTH device deliverables; derive them before the sim build
+ # overwrites bazel-bin/Telegram/Telegram.ipa with the simulator artifact.
+ build_device
+ make_sideload
+ make_livecontainer
+ build_sim
+ ;;
+ *)
+ die "unknown mode: $MODE"
+ ;;
+esac
+
+if [ "$INSTALL_SIM" -eq 1 ]; then
+ # --install forced MODE=sim above, so the simulator IPA was just built — install it.
+ install_sim
+fi
+
+echo
+echo "==> WinterGram deliverables in ./$OUT_DIR/:"
+ls -1 "$OUT_DIR"/WinterGram*.ipa 2>/dev/null | sed 's#^# #' || true
+
+if [ "$OPEN_BUILD_DIR" -eq 1 ]; then
+ open "$OUT_DIR"
+fi
\ No newline at end of file
diff --git a/scripts/generate-branding.sh b/scripts/generate-branding.sh
new file mode 100755
index 0000000000..4d46ae6180
--- /dev/null
+++ b/scripts/generate-branding.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+require_cmd() {
+ command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing command: $1" >&2; exit 1; }
+}
+
+require_cmd sips
+require_cmd python3
+
+BRANDING_DIR="branding"
+ALTICON_BASE="Telegram/Telegram-iOS"
+IMAGESET_BASE="Telegram/Telegram-iOS/AppIcons.xcassets"
+DEFAULT_ASSET_BASE="Telegram/Telegram-iOS/DefaultAppIcon.xcassets"
+PRIMARY_APPICONSET="$DEFAULT_ASSET_BASE/WinterGramDarkIcon.appiconset"
+
+resize_square() {
+ sips -s format png -z "$3" "$3" "$1" --out "$2" >/dev/null
+}
+
+resize_rect() {
+ sips -s format png -z "$4" "$3" "$1" --out "$2" >/dev/null
+}
+
+capitalize_words() {
+ python3 - "$1" <<'PY'
+import re, sys
+value = sys.argv[1]
+print("".join(part[:1].upper() + part[1:] for part in re.split(r"[-_]+", value) if part))
+PY
+}
+
+write_imageset_contents() {
+ local dir="$1" name="$2"
+ cat > "$dir/Contents.json" <.png" >&2
+ exit 1
+fi
+
+wintergram_icons=()
+for src in "${icon_sources[@]}"; do
+ base="$(basename "$src" .png)"
+ variant="${base#icon-app-}"
+ setname="WinterGram$(capitalize_words "$variant")"
+ wintergram_icons+=("$setname")
+ echo "==> icon $setname from $src"
+ generate_alticon "$src" "$setname"
+ generate_icon_imageset "$src" "$setname"
+ if [ "$variant" = "dark" ]; then
+ generate_primary_appiconset "$src"
+ fi
+done
+
+banner_assets=()
+for src in "${banner_sources[@]}"; do
+ base="$(basename "$src" .png)"
+ variant="${base#banner-}"
+ if [ "$variant" = "wintergram" ]; then
+ asset="WntGramBanner"
+ else
+ asset="WntGramBanner$(capitalize_words "$variant")"
+ fi
+ banner_assets+=("$asset")
+ echo "==> banner $asset from $src"
+ generate_banner_imageset "$src" "$asset"
+done
+
+python3 - "${wintergram_icons[@]}" <<'PY'
+import pathlib, re, sys
+icons = sys.argv[1:]
+
+build = pathlib.Path("Telegram/BUILD")
+text = build.read_text()
+match = re.search(r"alternate_icon_folders = \[\n(.*?)\n\]", text, re.S)
+if match:
+ existing = re.findall(r'"([^"]+)"', match.group(1))
+ non_wintergram = [name for name in existing if not name.startswith("WinterGram") and pathlib.Path(f"Telegram/Telegram-iOS/{name}.alticon").is_dir()]
+ names = sorted(non_wintergram + icons)
+ block = "alternate_icon_folders = [\n" + "".join(f' "{name}",\n' for name in names) + "]"
+ text = text[:match.start()] + block + text[match.end():]
+ build.write_text(text)
+
+app_delegate = pathlib.Path("submodules/TelegramUI/Sources/AppDelegate.swift")
+text = app_delegate.read_text()
+pattern = r'var icons = \[\n(.*?)PresentationAppIcon\(name: "BlueIcon"'
+match = re.search(pattern, text, re.S)
+if match:
+ wintergram_lines = "".join(f' PresentationAppIcon(name: "{name}", imageName: "{name}"),\n' for name in icons)
+ replacement = "var icons = [\n" + wintergram_lines + ' PresentationAppIcon(name: "BlueIcon"'
+ text = text[:match.start()] + replacement + text[match.end():]
+ app_delegate.write_text(text)
+PY
+
+echo
+echo "==> Generated WinterGram icons: ${wintergram_icons[*]}"
+if [ "${#banner_assets[@]}" -gt 0 ]; then
+ echo "==> Generated banners: ${banner_assets[*]}"
+fi
diff --git a/scripts/generate_wintergram_icon.swift b/scripts/generate_wintergram_icon.swift
new file mode 100644
index 0000000000..05d7e7aafa
--- /dev/null
+++ b/scripts/generate_wintergram_icon.swift
@@ -0,0 +1,70 @@
+#!/usr/bin/env swift
+import Foundation
+import CoreGraphics
+import ImageIO
+
+let projectRoot = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+let backplateURL = projectRoot.appendingPathComponent("branding/backplate_badge.png")
+let snowflakeURL = projectRoot.appendingPathComponent("branding/snowflake_monochrome.png")
+let outputDir = projectRoot.appendingPathComponent("submodules/TelegramUI/Images.xcassets/Item List/Icons/WinterGram.imageset")
+
+func loadImage(url: URL) -> CGImage? {
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
+ return CGImageSourceCreateImageAtIndex(source, 0, nil)
+}
+
+func saveImage(_ image: CGImage, url: URL) {
+ guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else {
+ fatalError("Cannot create PNG destination for \(url.path)")
+ }
+ CGImageDestinationAddImage(destination, image, nil)
+ guard CGImageDestinationFinalize(destination) else {
+ fatalError("Failed to write \(url.path)")
+ }
+}
+
+func composedBadge(backplate: CGImage, snowflake: CGImage, pixelSize: CGFloat) -> CGImage? {
+ let size = CGSize(width: pixelSize, height: pixelSize)
+ guard let context = CGContext(
+ data: nil,
+ width: Int(pixelSize),
+ height: Int(pixelSize),
+ bitsPerComponent: 8,
+ bytesPerRow: 0,
+ space: CGColorSpaceCreateDeviceRGB(),
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
+ ) else { return nil }
+
+ context.clear(CGRect(origin: .zero, size: size))
+
+ // Backplate fills the badge canvas (matches the runtime badge composition).
+ context.draw(backplate, in: CGRect(origin: .zero, size: size))
+
+ // Snowflake is 756/1024 of the badge, centred.
+ let snowflakeSize = pixelSize * 756.0 / 1024.0
+ let snowflakeOffset = (pixelSize - snowflakeSize) / 2.0
+ context.draw(snowflake, in: CGRect(x: snowflakeOffset, y: snowflakeOffset, width: snowflakeSize, height: snowflakeSize))
+
+ return context.makeImage()
+}
+
+guard let backplate = loadImage(url: backplateURL) else {
+ fatalError("Cannot load backplate at \(backplateURL.path)")
+}
+guard let snowflake = loadImage(url: snowflakeURL) else {
+ fatalError("Cannot load snowflake at \(snowflakeURL.path)")
+}
+
+let sizes: [(name: String, px: CGFloat)] = [
+ ("wintergram_snowflake_30@2x.png", 60.0),
+ ("wintergram_snowflake_30@3x.png", 90.0)
+]
+
+for (name, px) in sizes {
+ guard let image = composedBadge(backplate: backplate, snowflake: snowflake, pixelSize: px) else {
+ fatalError("Failed to compose \(name)")
+ }
+ let url = outputDir.appendingPathComponent(name)
+ saveImage(image, url: url)
+ print("Wrote \(url.path)")
+}
diff --git a/submodules/AvatarNode/BUILD b/submodules/AvatarNode/BUILD
index 3aad72c509..c366835308 100644
--- a/submodules/AvatarNode/BUILD
+++ b/submodules/AvatarNode/BUILD
@@ -14,6 +14,7 @@ swift_library(
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox",
+ "//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AnimationUI:AnimationUI",
"//submodules/AppBundle:AppBundle",
diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift
index e690da30ad..fc3ae5828f 100644
--- a/submodules/AvatarNode/Sources/AvatarNode.swift
+++ b/submodules/AvatarNode/Sources/AvatarNode.swift
@@ -6,6 +6,7 @@ import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
+import TelegramUIPreferences
import AnimationUI
import AppBundle
import AccountContext
@@ -50,7 +51,7 @@ private class AvatarNodeParameters: NSObject {
let hasImage: Bool
let clipStyle: AvatarNodeClipStyle
let cutoutRect: CGRect?
-
+
init(theme: PresentationTheme?, accountPeerId: EnginePeer.Id?, peerId: EnginePeer.Id?, colors: [UIColor], letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool, clipStyle: AvatarNodeClipStyle, cutoutRect: CGRect?) {
self.theme = theme
self.accountPeerId = accountPeerId
@@ -63,10 +64,10 @@ private class AvatarNodeParameters: NSObject {
self.hasImage = hasImage
self.clipStyle = clipStyle
self.cutoutRect = cutoutRect
-
+
super.init()
}
-
+
func withUpdatedHasImage(_ hasImage: Bool) -> AvatarNodeParameters {
return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, colors: self.colors, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage, clipStyle: self.clipStyle, cutoutRect: self.cutoutRect)
}
@@ -87,7 +88,7 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
colorIndex = -1
}
}
-
+
let colors: [UIColor]
if icon != .none {
if case .deletedIcon = icon {
@@ -158,7 +159,7 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
}
return index
}
-
+
switch nameColor {
case let .preset(nameColor):
if let context, nameColor.rawValue > 13 {
@@ -173,12 +174,12 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
let index = colorIndexFromColor(color: color)
colors = AvatarNode.gradientColors[index % AvatarNode.gradientColors.count]
}
-
+
} else {
colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
}
}
-
+
return colors
}
@@ -243,36 +244,36 @@ public enum AvatarNodeColorOverride {
public final class AvatarEditOverlayNode: ASDisplayNode {
override public init() {
super.init()
-
+
self.isOpaque = false
self.displaysAsynchronously = true
}
-
+
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
-
+
let context = UIGraphicsGetCurrentContext()!
-
+
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
-
+
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
bounds.size.height))
context.clip()
-
+
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
context.fill(bounds)
-
+
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
context.setBlendMode(.normal)
-
+
if bounds.width > 90.0 {
if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
@@ -298,9 +299,9 @@ public final class AvatarNode: ASDisplayNode {
context.fillPath()
})
}
-
+
public static let avatarBubblePath: CGPath = generateAvatarBubblePath()
-
+
public static func addAvatarBubblePath(context: CGContext, rect: CGRect) {
let path = AvatarNode.avatarBubblePath
let sx = rect.width / 60.0
@@ -314,7 +315,7 @@ public final class AvatarNode: ASDisplayNode {
let transformedPath = path.copy(using: &transform)!
context.addPath(transformedPath)
}
-
+
public static let gradientColors: [[UIColor]] = [
[UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)],
[UIColor(rgb: 0xffa85c), UIColor(rgb: 0xffcd6a)],
@@ -324,30 +325,30 @@ public final class AvatarNode: ASDisplayNode {
[UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)],
[UIColor(rgb: 0xd669ed), UIColor(rgb: 0xe0a2f3)],
]
-
+
static let grayscaleColors: [UIColor] = [
UIColor(rgb: 0xb1b1b1), UIColor(rgb: 0xcdcdcd)
]
-
+
static let grayscaleDarkColors: [UIColor] = [
UIColor(white: 1.0, alpha: 0.22), UIColor(white: 1.0, alpha: 0.18)
]
-
+
static let savedMessagesColors: [UIColor] = [
UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)
]
-
+
static let repostColors: [UIColor] = [
UIColor(rgb: 0x3DA1FD), UIColor(rgb: 0x34C76F)
]
-
+
public final class ContentNode: ASDisplayNode {
private struct Params: Equatable {
let peerId: EnginePeer.Id?
let resourceId: String?
let displayDimensions: CGSize
let clipStyle: AvatarNodeClipStyle
-
+
init(
peerId: EnginePeer.Id?,
resourceId: String?,
@@ -360,14 +361,14 @@ public final class AvatarNode: ASDisplayNode {
self.clipStyle = clipStyle
}
}
-
+
public var font: UIFont {
didSet {
if oldValue.pointSize != font.pointSize {
if let parameters = self.parameters {
self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, colors: parameters.colors, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage, clipStyle: parameters.clipStyle, cutoutRect: parameters.cutoutRect)
}
-
+
if !self.displaySuspended {
self.setNeedsDisplay()
}
@@ -380,16 +381,16 @@ public final class AvatarNode: ASDisplayNode {
public let imageNode: ImageNode
private var imageNodeMask: UIImageView?
public var editOverlayNode: AvatarEditOverlayNode?
-
+
private let imageReadyDisposable = MetaDisposable()
fileprivate var state: AvatarNodeState = .empty
-
+
public var unroundedImage: UIImage?
private var currentImage: UIImage?
-
+
private var params: Params?
private var loadDisposable = MetaDisposable()
-
+
var clipStyle: AvatarNodeClipStyle {
if let params = self.params {
return params.clipStyle
@@ -398,7 +399,7 @@ public final class AvatarNode: ASDisplayNode {
}
return .none
}
-
+
public var badgeView: AvatarBadgeView? {
didSet {
if self.badgeView !== oldValue {
@@ -420,7 +421,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
private let imageReady = Promise(false)
public var ready: Signal {
let imageReady = self.imageReady
@@ -432,49 +433,49 @@ public final class AvatarNode: ASDisplayNode {
})
}
}
-
+
public init(font: UIFont) {
self.font = font
self.imageNode = ImageNode(enableHasImage: true, enableAnimatedTransition: true)
-
+
super.init()
-
+
self.isOpaque = false
self.displaysAsynchronously = true
self.disableClearContentsOnHide = true
-
+
self.imageNode.isUserInteractionEnabled = false
self.addSubnode(self.imageNode)
-
+
self.imageNode.contentUpdated = { [weak self] image in
guard let self else {
return
}
-
+
self.currentImage = image
-
+
guard let badgeView = self.badgeView, let parameters = self.parameters else {
return
}
-
+
if parameters.hasImage, let image {
badgeView.update(content: .image(image))
}
}
}
-
+
deinit {
self.loadDisposable.dispose()
}
-
+
override public func didLoad() {
super.didLoad()
-
+
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), !self.isLayerBacked {
self.view.accessibilityIgnoresInvertColors = true
}
}
-
+
public func updateSize(size: CGSize) {
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.editOverlayNode?.frame = self.imageNode.frame
@@ -486,12 +487,12 @@ public final class AvatarNode: ASDisplayNode {
self.editOverlayNode?.setNeedsDisplay()
}
}
-
+
public func playArchiveAnimation() {
guard let theme = self.theme else {
return
}
-
+
var iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
var backgroundColor = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.topColor
let animationBackgroundNode = ASImageNode()
@@ -510,9 +511,9 @@ public final class AvatarNode: ASDisplayNode {
backgroundColor = backgroundColors.1.mixedWith(backgroundColors.0, alpha: 0.5)
animationBackgroundNode.image = generateGradientFilledCircleImage(diameter: self.imageNode.frame.width, colors: colors)
}
-
+
self.addSubnode(animationBackgroundNode)
-
+
let animationNode = AnimationNode(animation: "anim_archiveAvatar", colors: ["box1.box1.Fill 1": iconColor, "box3.box3.Fill 1": iconColor, "box2.box2.Fill 1": backgroundColor], scale: 0.1653828)
animationNode.isUserInteractionEnabled = false
animationNode.completion = { [weak animationBackgroundNode, weak self] in
@@ -520,11 +521,11 @@ public final class AvatarNode: ASDisplayNode {
animationBackgroundNode?.removeFromSupernode()
}
animationBackgroundNode.addSubnode(animationNode)
-
+
animationBackgroundNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.12, removeOnCompletion: false, completion: { [weak animationBackgroundNode] finished in
animationBackgroundNode?.layer.animateScale(from: 1.07, to: 1.0, duration: 0.12, removeOnCompletion: false)
})
-
+
if var size = animationNode.preferredSize() {
size = CGSize(width: ceil(size.width), height: ceil(size.height))
animationNode.frame = CGRect(x: floor((self.bounds.width - size.width) / 2.0), y: floor((self.bounds.height - size.height) / 2.0) + 1.0, width: size.width, height: size.height)
@@ -532,12 +533,12 @@ public final class AvatarNode: ASDisplayNode {
}
self.imageNode.isHidden = true
}
-
+
public func playRepostAnimation() {
let animationNode = AnimationNode(animation: "anim_storyrepost", colors: [:], scale: 0.11)
animationNode.isUserInteractionEnabled = false
self.addSubnode(animationNode)
-
+
if var size = animationNode.preferredSize() {
size = CGSize(width: ceil(size.width), height: ceil(size.height))
animationNode.frame = CGRect(x: floor((self.bounds.width - size.width) / 2.0), y: floor((self.bounds.height - size.height) / 2.0) + 1.0, width: size.width, height: size.height)
@@ -546,18 +547,18 @@ public final class AvatarNode: ASDisplayNode {
})
}
}
-
+
public func playCameraAnimation() {
let animationBackgroundNode = ASImageNode()
animationBackgroundNode.isUserInteractionEnabled = false
animationBackgroundNode.frame = self.imageNode.frame
animationBackgroundNode.image = generateGradientFilledCircleImage(diameter: self.imageNode.frame.width, colors: AvatarNode.repostColors.map { $0.cgColor } as NSArray)
self.addSubnode(animationBackgroundNode)
-
+
let animationNode = AnimationNode(animation: "anim_camera", colors: [:], scale: 0.082)
animationNode.isUserInteractionEnabled = false
self.addSubnode(animationNode)
-
+
if var size = animationNode.preferredSize() {
size = CGSize(width: ceil(size.width), height: ceil(size.height))
animationNode.frame = CGRect(x: floor((self.bounds.width - size.width) / 2.0) + 1.0, y: floor((self.bounds.height - size.height) / 2.0), width: size.width, height: size.height)
@@ -570,7 +571,7 @@ public final class AvatarNode: ASDisplayNode {
})
}
}
-
+
public func setPeer(
accountPeerId: EnginePeer.Id,
postbox: Postbox,
@@ -634,15 +635,14 @@ public final class AvatarNode: ASDisplayNode {
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
representation = peer?.smallProfileImage
}
-
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle, cutoutRect)
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
self.state = updatedState
self.overrideImage = overrideImage
self.theme = theme
-
+
let parameters: AvatarNodeParameters
-
+
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, 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
@@ -655,21 +655,21 @@ public final class AvatarNode: ASDisplayNode {
|> map { next -> UIImage? in
return next?.0
})
-
+
if case .editAvatarIcon = icon {
if self.editOverlayNode == nil {
let editOverlayNode = AvatarEditOverlayNode()
editOverlayNode.frame = self.imageNode.frame
editOverlayNode.isUserInteractionEnabled = false
self.addSubnode(editOverlayNode)
-
+
self.editOverlayNode = editOverlayNode
}
self.editOverlayNode?.isHidden = false
} else {
self.editOverlayNode?.isHidden = true
}
-
+
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle, cutoutRect: cutoutRect)
} else {
self.imageReady.set(.single(true))
@@ -677,11 +677,11 @@ public final class AvatarNode: ASDisplayNode {
if self.isNodeLoaded {
self.imageNode.contents = nil
}
-
+
self.editOverlayNode?.isHidden = true
let colors = calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle, cutoutRect: cutoutRect)
-
+
if let badgeView = self.badgeView {
let badgeColor: UIColor
if colors.isEmpty {
@@ -701,7 +701,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
func setPeerV2(
context genericContext: AccountContext,
account: Account? = nil,
@@ -727,21 +727,22 @@ public final class AvatarNode: ASDisplayNode {
}
let previousSize = self.params?.displayDimensions
self.params = params
-
+
switch clipStyle {
case .none:
self.imageNode.clipsToBounds = false
self.imageNode.cornerRadius = 0.0
case .round:
self.imageNode.clipsToBounds = true
- self.imageNode.cornerRadius = displayDimensions.height * 0.5
+ // WinterGram: avatarCornerRadius (0=square ... 50=fully round) as a percentage of height.
+ self.imageNode.cornerRadius = displayDimensions.height * (CGFloat(currentWinterGramSettings.avatarCornerRadius) / 100.0)
case .roundedRect:
self.imageNode.clipsToBounds = true
self.imageNode.cornerRadius = displayDimensions.height * 0.25
case .bubble:
break
}
-
+
if case .bubble = clipStyle {
var updateMask = false
let imageNodeMask: UIImageView
@@ -762,7 +763,7 @@ public final class AvatarNode: ASDisplayNode {
self.imageNodeMask = nil
self.imageNode.view.mask = nil
}
-
+
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 {
@@ -779,7 +780,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
public func setPeer(
context genericContext: AccountContext,
account: Account? = nil,
@@ -841,17 +842,16 @@ public final class AvatarNode: ASDisplayNode {
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage
}
-
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle, cutoutRect)
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
self.state = updatedState
self.overrideImage = overrideImage
self.theme = theme
-
+
let parameters: AvatarNodeParameters
-
+
let account = account ?? genericContext.account
-
+
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
@@ -864,21 +864,21 @@ public final class AvatarNode: ASDisplayNode {
|> map { next -> UIImage? in
return next?.0
})
-
+
if case .editAvatarIcon = icon {
if self.editOverlayNode == nil {
let editOverlayNode = AvatarEditOverlayNode()
editOverlayNode.frame = self.imageNode.frame
editOverlayNode.isUserInteractionEnabled = false
self.addSubnode(editOverlayNode)
-
+
self.editOverlayNode = editOverlayNode
}
self.editOverlayNode?.isHidden = false
} else {
self.editOverlayNode?.isHidden = true
}
-
+
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle, cutoutRect: cutoutRect)
} else {
self.imageReady.set(.single(true))
@@ -886,11 +886,11 @@ public final class AvatarNode: ASDisplayNode {
if self.isNodeLoaded {
self.imageNode.contents = nil
}
-
+
self.editOverlayNode?.isHidden = true
let colors = calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle, cutoutRect: cutoutRect)
-
+
if let badgeView = self.badgeView {
let badgeColor: UIColor
if colors.isEmpty {
@@ -910,7 +910,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil, cutoutRect: CGRect? = nil) {
var explicitIndex: Int?
if let explicitColor = explicitColor {
@@ -922,52 +922,57 @@ public final class AvatarNode: ASDisplayNode {
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
if updatedState != self.state {
self.state = updatedState
-
+
let parameters: AvatarNodeParameters
if let icon = icon, case .phone = icon {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round, cutoutRect: cutoutRect)
} else {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round, cutoutRect: cutoutRect)
}
-
+
self.displaySuspended = true
self.contents = nil
-
+
self.imageReady.set(.single(true))
self.displaySuspended = false
-
+
if self.parameters == nil || self.parameters != parameters {
self.parameters = parameters
self.setNeedsDisplay()
}
}
}
-
+
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return parameters ?? NSObject()
}
-
+
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
-
+
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
-
+
if !(parameters is AvatarNodeParameters) {
return
}
-
+
let colors: [UIColor]
if let parameters = parameters as? AvatarNodeParameters {
colors = parameters.colors
-
+
if case .round = parameters.clipStyle {
context.beginPath()
- context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
- bounds.size.height))
+ // WinterGram: honor avatarCornerRadius for letter-placeholder avatars too.
+ let wntRadiusFraction = CGFloat(currentWinterGramSettings.avatarCornerRadius) / 100.0
+ if wntRadiusFraction >= 0.5 {
+ context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height))
+ } else {
+ context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height), cornerRadius: floor(bounds.size.height * wntRadiusFraction)).cgPath)
+ }
context.clip()
} else if case .roundedRect = parameters.clipStyle {
context.beginPath()
@@ -981,9 +986,9 @@ public final class AvatarNode: ASDisplayNode {
} else {
colors = grayscaleColors
}
-
+
let colorsArray: NSArray = colors.map(\.cgColor) as NSArray
-
+
var iconColor = UIColor.white
var diagonal = false
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
@@ -998,27 +1003,27 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
var locations: [CGFloat] = [1.0, 0.0]
-
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
-
+
if diagonal {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: bounds.size.height), end: CGPoint(x: bounds.size.width, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
}
-
+
context.setBlendMode(.normal)
-
+
if let parameters = parameters as? AvatarNodeParameters {
if case .deletedIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let deletedIcon = deletedIcon {
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
}
@@ -1027,7 +1032,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let phoneIcon = phoneIcon {
context.draw(phoneIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - phoneIcon.size.width) / 2.0), y: floor((bounds.size.height - phoneIcon.size.height) / 2.0)), size: phoneIcon.size))
}
@@ -1036,7 +1041,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let savedMessagesIcon = savedMessagesIcon {
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
}
@@ -1046,7 +1051,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let repostStoryIcon = repostStoryIcon {
context.draw(repostStoryIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repostStoryIcon.size.width) / 2.0), y: floor((bounds.size.height - repostStoryIcon.size.height) / 2.0)), size: repostStoryIcon.size))
}
@@ -1056,7 +1061,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let repliesIcon = repliesIcon {
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
}
@@ -1065,7 +1070,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let theme = parameters.theme, theme.overallDarkAppearance, !isColored {
if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon {
context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size))
@@ -1080,7 +1085,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let myNotesIcon = myNotesIcon {
context.draw(myNotesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - myNotesIcon.size.width) / 2.0), y: floor((bounds.size.height - myNotesIcon.size.height) / 2.0)), size: myNotesIcon.size))
}
@@ -1089,7 +1094,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let cameraIcon = cameraIcon {
context.draw(cameraIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - cameraIcon.size.width) / 2.0), y: floor((bounds.size.height - cameraIcon.size.height) / 2.0)), size: cameraIcon.size))
}
@@ -1098,7 +1103,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let storyIcon = storyIcon {
context.draw(storyIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - storyIcon.size.width) / 2.0), y: floor((bounds.size.height - storyIcon.size.height) / 2.0)), size: storyIcon.size))
}
@@ -1106,7 +1111,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if bounds.width > 90.0, let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: theme.list.itemAccentColor) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
} else if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.itemAccentColor) {
@@ -1117,7 +1122,7 @@ public final class AvatarNode: ASDisplayNode {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
if let archivedChatsIcon = generateTintedImage(image: archivedChatsIcon, color: iconColor) {
context.draw(archivedChatsIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - archivedChatsIcon.size.width) / 2.0), y: floor((bounds.size.height - archivedChatsIcon.size.height) / 2.0)), size: archivedChatsIcon.size))
}
@@ -1126,26 +1131,26 @@ public final class AvatarNode: ASDisplayNode {
if letters.count == 2 && letters[0].isSingleEmoji && letters[1].isSingleEmoji {
letters = [letters[0]]
}
-
+
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: parameters.font, NSAttributedString.Key.foregroundColor: UIColor.white])
-
+
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
-
+
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0))
-
+
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
-
+
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
}
}
-
+
if let parameters = parameters as? AvatarNodeParameters, let cutoutRect = parameters.cutoutRect {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
@@ -1153,20 +1158,20 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
public let contentNode: ContentNode
private var storyIndicator: ComponentView?
public private(set) var storyPresentationParams: StoryPresentationParams?
-
+
private var loadingStatuses = Bag()
-
+
public struct StoryStats: Equatable {
public var totalCount: Int
public var unseenCount: Int
public var hasUnseenCloseFriendsItems: Bool
public var hasLiveItems: Bool
public var progress: Float?
-
+
public init(
totalCount: Int,
unseenCount: Int,
@@ -1181,9 +1186,9 @@ public final class AvatarNode: ASDisplayNode {
self.progress = progress
}
}
-
+
public private(set) var storyStats: StoryStats?
-
+
public var font: UIFont {
get {
return self.contentNode.font
@@ -1191,7 +1196,7 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.font = value
}
}
-
+
public var editOverlayNode: AvatarEditOverlayNode? {
get {
return self.contentNode.editOverlayNode
@@ -1199,7 +1204,7 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.editOverlayNode = value
}
}
-
+
public var unroundedImage: UIImage? {
get {
return self.contentNode.unroundedImage
@@ -1207,7 +1212,7 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.unroundedImage = value
}
}
-
+
public var badgeView: AvatarBadgeView? {
get {
return self.contentNode.badgeView
@@ -1215,72 +1220,72 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.badgeView = value
}
}
-
+
public var ready: Signal {
return self.contentNode.ready
}
-
+
public var imageNode: ImageNode {
return self.contentNode.imageNode
}
-
+
public init(font: UIFont) {
self.contentNode = ContentNode(font: font)
-
+
super.init()
-
+
self.onDidLoad { [weak self] _ in
guard let self else {
return
}
self.updateStoryIndicator(transition: .immediate)
}
-
+
self.addSubnode(self.contentNode)
}
-
+
deinit {
self.cancelLoading()
}
-
+
override public var frame: CGRect {
get {
return super.frame
} set(value) {
let updateImage = !value.size.equalTo(super.frame.size)
super.frame = value
-
+
if updateImage {
self.updateSize(size: value.size)
}
}
}
-
+
override public func nodeDidLoad() {
super.nodeDidLoad()
}
-
+
public func updateSize(size: CGSize) {
self.contentNode.position = CGRect(origin: CGPoint(), size: size).center
self.contentNode.bounds = CGRect(origin: CGPoint(), size: size)
-
+
self.contentNode.updateSize(size: size)
-
+
self.updateStoryIndicator(transition: .immediate)
}
-
+
public func playArchiveAnimation() {
self.contentNode.playArchiveAnimation()
}
-
+
public func playRepostAnimation() {
self.contentNode.playRepostAnimation()
}
-
+
public func playCameraAnimation() {
self.contentNode.playCameraAnimation ()
}
-
+
public func setPeer(
accountPeerId: EnginePeer.Id,
postbox: Postbox,
@@ -1312,7 +1317,7 @@ public final class AvatarNode: ASDisplayNode {
storeUnrounded: storeUnrounded
)
}
-
+
public func setPeerV2(
context genericContext: AccountContext,
theme: PresentationTheme,
@@ -1338,7 +1343,7 @@ public final class AvatarNode: ASDisplayNode {
storeUnrounded: storeUnrounded
)
}
-
+
public func setPeer(
context: AccountContext,
account: Account? = nil,
@@ -1368,25 +1373,25 @@ public final class AvatarNode: ASDisplayNode {
cutoutRect: cutoutRect
)
}
-
+
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon)
}
-
+
public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: ComponentTransition) {
if self.storyStats != storyStats || self.storyPresentationParams != presentationParams {
self.storyStats = storyStats
self.storyPresentationParams = presentationParams
-
+
self.updateStoryIndicator(transition: transition)
}
}
-
+
public struct Colors: Equatable {
public var unseenColors: [UIColor]
public var unseenCloseFriendsColors: [UIColor]
public var seenColors: [UIColor]
-
+
public init(
unseenColors: [UIColor],
unseenCloseFriendsColors: [UIColor],
@@ -1396,20 +1401,20 @@ public final class AvatarNode: ASDisplayNode {
self.unseenCloseFriendsColors = unseenCloseFriendsColors
self.seenColors = seenColors
}
-
+
public init(theme: PresentationTheme) {
self.unseenColors = [theme.chatList.storyUnseenColors.topColor, theme.chatList.storyUnseenColors.bottomColor]
self.unseenCloseFriendsColors = [theme.chatList.storyUnseenPrivateColors.topColor, theme.chatList.storyUnseenPrivateColors.bottomColor]
self.seenColors = [theme.chatList.storySeenColors.topColor, theme.chatList.storySeenColors.bottomColor]
}
}
-
+
public struct StoryPresentationParams: Equatable {
public var colors: Colors
public var lineWidth: CGFloat
public var inactiveLineWidth: CGFloat
public var forceRoundedRect: Bool
-
+
public init(
colors: Colors,
lineWidth: CGFloat,
@@ -1422,7 +1427,7 @@ public final class AvatarNode: ASDisplayNode {
self.forceRoundedRect = forceRoundedRect
}
}
-
+
private func updateStoryIndicator(transition: ComponentTransition) {
if !self.isNodeLoaded {
return
@@ -1433,15 +1438,15 @@ public final class AvatarNode: ASDisplayNode {
guard let storyPresentationParams = self.storyPresentationParams else {
return
}
-
+
let size = self.bounds.size
-
+
if let storyStats = self.storyStats {
let activeLineWidth = storyPresentationParams.lineWidth
let inactiveLineWidth = storyPresentationParams.inactiveLineWidth
let indicatorSize = CGSize(width: size.width - activeLineWidth * 4.0, height: size.height - activeLineWidth * 4.0)
let avatarScale = (size.width - activeLineWidth * 4.0) / size.width
-
+
let storyIndicator: ComponentView
var indicatorTransition = transition
if let current = self.storyIndicator {
@@ -1499,7 +1504,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
+
public func cancelLoading() {
for disposable in self.loadingStatuses.copyItems() {
disposable.dispose()
@@ -1507,21 +1512,21 @@ public final class AvatarNode: ASDisplayNode {
self.loadingStatuses.removeAll()
self.updateStoryIndicator(transition: .immediate)
}
-
+
public func pushLoadingStatus(signal: Signal) -> Disposable {
let disposable = MetaDisposable()
-
+
for d in self.loadingStatuses.copyItems() {
d.dispose()
}
self.loadingStatuses.removeAll()
-
+
let index = self.loadingStatuses.add(disposable)
-
+
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
self?.updateStoryIndicator(transition: .immediate)
})
-
+
disposable.set(signal.start(completed: { [weak self] in
Queue.mainQueue().async {
guard let self else {
@@ -1536,7 +1541,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
}))
-
+
return ActionDisposable { [weak self] in
guard let self else {
return
@@ -1551,4 +1556,3 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
-
diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift
index 0d0908f79e..3d5ebdd8af 100644
--- a/submodules/ChatListUI/Sources/ChatContextMenus.swift
+++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift
@@ -27,7 +27,7 @@ func archiveContextMenuItems(context: AccountContext, group: EngineChatList.Grou
)
|> map { [weak chatListController] unreadChatListPeerIds, chatArchiveSettingsPreference -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
-
+
if !unreadChatListPeerIds.isEmpty {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAllAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = (context.engine.messages.markAllChatsAsReadInteractively(items: [(groupId: group, filterPredicate: nil)])
@@ -36,14 +36,14 @@ func archiveContextMenuItems(context: AccountContext, group: EngineChatList.Grou
})
})))
}
-
+
let settings = chatArchiveSettingsPreference?.get(ChatArchiveSettings.self) ?? ChatArchiveSettings.default
let isPinned = !settings.isHiddenByDefault
items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_HideArchive : strings.ChatList_Context_UnhideArchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] _, f in
chatListController?.toggleArchivedFolderHiddenByDefault()
f(.default)
})))
-
+
return items
}
}
@@ -87,9 +87,9 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
)
|> mapToSignal { filters, pinnedItemIds -> Signal<[ContextMenuItem], NoError> in
let isPinned = pinnedItemIds.contains(.peer(peerId))
-
+
let renderedPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.RenderedPeer(id: peerId))
-
+
return renderedPeer
|> mapToSignal { renderedPeer -> Signal<[ContextMenuItem], NoError> in
guard let renderedPeer = renderedPeer else {
@@ -98,7 +98,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
guard let peer = renderedPeer.chatMainPeer else {
return .single([])
}
-
+
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.IsContact(id: peer.id),
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id),
@@ -151,7 +151,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
} else if case .search(.popularApps) = source {
} else {
let isSavedMessages = peerId == context.account.peerId
-
+
if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted {
if !isContact {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in
@@ -169,7 +169,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
items.append(.separator)
}
}
-
+
var isMuted = false
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
isMuted = true
@@ -187,17 +187,17 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
}
}
}
-
+
var isUnread = false
if readCounters.isUnread {
isUnread = true
}
-
+
var isForum = false
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
isForum = true
}
-
+
var hasRemoveFromFolder = false
if case let .chatList(currentFilter) = source {
if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter {
@@ -225,46 +225,46 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
hasRemoveFromFolder = true
}
}
-
+
if !hasRemoveFromFolder && peerGroup != nil {
var hasFolders = false
-
+
for case let .filter(_, _, _, data) in filters {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
continue
}
-
+
var data = data
if data.addIncludePeer(peerId: peer.id) {
hasFolders = true
break
}
}
-
+
if hasFolders {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
var updatedItems: [ContextMenuItem] = []
-
+
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c, _ in
c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
})))
updatedItems.append(.separator)
-
+
for filter in filters {
if case let .filter(_, title, _, data) = filter {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
continue
}
-
+
var data = data
if !data.addIncludePeer(peerId: peer.id) {
continue
}
-
+
let filterType = chatListFilterType(data)
updatedItems.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, icon: { theme in
let imageName: String
@@ -291,10 +291,10 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
c?.dismiss(completion: {
let isPremium = limitsData.0?.isPremium ?? false
let (_, limits, premiumLimits) = limitsData
-
+
let limit = limits.maxFolderChatsCount
let premiumLimit = premiumLimits.maxFolderChatsCount
-
+
let count = data.includePeers.peers.count - 1
if count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
@@ -315,7 +315,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
chatListController?.push(controller)
return
}
-
+
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
for i in 0 ..< filters.count {
@@ -337,13 +337,13 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
})))
}
}
-
+
c?.setItems(.single(ContextController.Items(content: .list(updatedItems), context: context)), minHeight: nil, animated: true)
})))
items.append(.separator)
}
}
-
+
if isUnread {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone()
@@ -361,7 +361,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
})))
}
}
-
+
let archiveEnabled = !isSavedMessages && peerId != EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId
if let group = peerGroup {
if archiveEnabled {
@@ -385,7 +385,49 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
}
})))
}
-
+
+ if !isSavedMessages && peerId != context.account.peerId {
+ let rawPeerId = peerId.toInt64()
+ let isStashed = currentWinterGramSettings.stashedPeerIds.contains(rawPeerId)
+ items.append(.action(ContextMenuActionItem(text: isStashed ? "Remove from Hidden Archive" : "Add to Hidden Archive", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isStashed ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { c, f in
+ let willStash = !isStashed
+ let _ = updateWinterGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
+ var settings = settings
+ if isStashed {
+ settings.stashedPeerIds.removeAll(where: { $0 == rawPeerId })
+ } else if !settings.stashedPeerIds.contains(rawPeerId) {
+ settings.stashedPeerIds.append(rawPeerId)
+ }
+ return settings
+ }).startStandalone()
+ // Mirror the change into the selected privacy categories' "Everybody Except..."
+ // lists so the stashed peer cannot see the chosen profile data.
+ if currentWinterGramSettings.stashPrivacy.hasAny || isStashed {
+ let _ = winterGramApplyStashPrivacy(engine: context.engine, peerId: peerId, stashed: willStash, privacySettings: currentWinterGramSettings.stashPrivacy).startStandalone()
+ }
+ if willStash {
+ let peerTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
+ c?.dismiss(completion: {
+ chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_archiveswipe", scale: 1.0, colors: [:], title: "Hidden Archive", text: "Вы добавили \(peerTitle) в скрытый архив", customUndoText: presentationData.strings.Undo_Undo, timeout: 5.0), elevatedLayout: false, animateInAsReplacement: true, action: { action in
+ if case .undo = action {
+ let _ = updateWinterGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
+ var settings = settings
+ settings.stashedPeerIds.removeAll(where: { $0 == rawPeerId })
+ return settings
+ }).startStandalone()
+ if currentWinterGramSettings.stashPrivacy.hasAny {
+ let _ = winterGramApplyStashPrivacy(engine: context.engine, peerId: peerId, stashed: false, privacySettings: currentWinterGramSettings.stashPrivacy).startStandalone()
+ }
+ }
+ return false
+ }), in: .current)
+ })
+ } else {
+ f(.default)
+ }
+ })))
+ }
+
if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat {
items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in
let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId))
@@ -395,7 +437,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
f(.default)
case let .limitExceeded(count, _):
f(.default)
-
+
let isPremium = limitsData.0?.isPremium ?? false
if isPremium {
if case .filter = location {
@@ -438,7 +480,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
})
})))
}
-
+
if !isSavedMessages {
var isMuted = false
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
@@ -491,7 +533,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
createSignal = createSignal
|> afterDisposed {
Queue.mainQueue().async {
@@ -502,7 +544,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
cancelImpl = {
joinChannelDisposable.set(nil)
}
-
+
var didJoin = false
joinChannelDisposable.set((createSignal
|> deliverOnMainQueue).start(next: { result in
@@ -538,7 +580,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
}
}
}
-
+
if case .chatList = source, peerGroup != nil {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
if let chatListController = chatListController {
@@ -566,7 +608,7 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
})))
}
}
-
+
if peerGroup != nil {
if !items.isEmpty {
if !addedSeparator {
@@ -615,9 +657,9 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
guard let threadData else {
return .single([])
}
-
+
var items: [ContextMenuItem] = []
-
+
var isCreator = false
var canManageTopics = false
var canDeleteMessages = false
@@ -630,7 +672,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
canManageTopics = true
canDeleteMessages = true
}
-
+
if let isClosed, isClosed && threadId != 1 {
} else {
if let isPinned, canManageTopics {
@@ -643,9 +685,9 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
}
return
}
-
+
f(.default)
-
+
let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId)
|> deliverOnMainQueue).startStandalone(error: { error in
switch error {
@@ -660,7 +702,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
}
})
})))
-
+
if isPinned, let reorder {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_InlineTopicMenu_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c?.dismiss(completion: {
@@ -670,19 +712,19 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
}
}
}
-
+
var isUnread = false
if threadData.incomingUnreadCount != 0 {
isUnread = true
}
-
+
if isUnread {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = context.engine.messages.markForumThreadAsRead(peerId: peerId, threadId: threadId).startStandalone()
f(.default)
})))
}
-
+
var canOpenClose = false
if case .channel = peer {
if isCreator {
@@ -693,7 +735,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
canOpenClose = true
}
}
-
+
if threadId != 1, canOpenClose, let customEdit {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_InlineTopicMenu_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in
if let c = c as? ContextController {
@@ -704,7 +746,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
return
})))
}
-
+
var isMuted = false
switch threadData.notificationSettings.muteState {
case .muted:
@@ -730,53 +772,53 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
})
} else {
var items: [ContextMenuItem] = []
-
+
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteFor, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Mute2d"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
var subItems: [ContextMenuItem] = []
-
+
/*subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c?.popItems()
})))
subItems.append(.separator)*/
-
+
let presetValues: [Int32] = [
1 * 60 * 60,
8 * 60 * 60,
1 * 24 * 60 * 60,
7 * 24 * 60 * 60
]
-
+
for value in presetValues {
subItems.append(.action(ContextMenuActionItem(text: muteForIntervalString(strings: presentationData.strings, value: value), icon: { _ in
return nil
}, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: value).startStandalone()
-
+
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedFor(mutedForTimeIntervalString(strings: presentationData.strings, value: value)).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
}
-
+
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteForCustom, icon: { _ in
return nil
}, action: { _, f in
f(.default)
-
+
if let chatListController = chatListController {
openCustomMute(context: context, peerId: peerId, threadId: threadId, baseController: chatListController)
}
})))
-
+
c?.setItems(.single(ContextController.Items(content: .list(subItems))), minHeight: nil, animated: true)
})))
-
+
items.append(.separator)
-
+
var isSoundEnabled = true
switch threadData.notificationSettings.messageSound {
case .none:
@@ -784,15 +826,15 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
default:
break
}
-
+
if case .muted = threadData.notificationSettings.muteState {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonUnmute, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: nil).startStandalone()
-
+
let iconColor: UIColor = .white
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
@@ -807,9 +849,9 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: .default).startStandalone()
-
+
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipSoundEnabled, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
} else {
@@ -817,18 +859,18 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOff"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: .none).startStandalone()
-
+
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_sound_off", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipSoundDisabled, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
}
-
+
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_NotificationsCustomize, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
-
+
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.NotificationSettings.Global()
)
@@ -836,40 +878,40 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal = { peerId, sound in
return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: sound) |> deliverOnMainQueue
}
-
+
let updatePeerNotificationInterval: (EnginePeer.Id, Int32?) -> Signal = { peerId, muteInterval in
return context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: muteInterval) |> deliverOnMainQueue
}
-
+
let updatePeerDisplayPreviews: (EnginePeer.Id, PeerNotificationDisplayPreviews) -> Signal = {
peerId, displayPreviews in
return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) |> deliverOnMainQueue
}
-
+
let updatePeerStoriesMuted: (EnginePeer.Id, PeerStoryNotificationSettings.Mute) -> Signal = {
peerId, mute in
return context.engine.peers.updatePeerStoriesMutedSetting(peerId: peerId, mute: mute) |> deliverOnMainQueue
}
-
+
let updatePeerStoriesHideSender: (EnginePeer.Id, PeerStoryNotificationSettings.HideSender) -> Signal = {
peerId, hideSender in
return context.engine.peers.updatePeerStoriesHideSenderSetting(peerId: peerId, hideSender: hideSender) |> deliverOnMainQueue
}
-
+
let updatePeerStorySound: (EnginePeer.Id, PeerMessageSound) -> Signal = { peerId, sound in
return context.engine.peers.updatePeerStorySoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue
}
-
+
let defaultSound: PeerMessageSound
-
+
if case let .channel(channel) = peer, case .broadcast = channel.info {
defaultSound = globalSettings.channels.sound._asMessageSound()
} else {
defaultSound = globalSettings.groupChats.sound._asMessageSound()
}
-
+
let canRemove = false
-
+
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: peer, threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peerId, sound)
|> deliverOnMainQueue).startStandalone(next: { _ in
@@ -891,7 +933,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
}, updatePeerDisplayPreviews: { peerId, displayPreviews in
let _ = (updatePeerDisplayPreviews(peerId, displayPreviews)
|> deliverOnMainQueue).startStandalone(next: { _ in
-
+
})
}, updatePeerStoriesMuted: { peerId, mute in
let _ = (updatePeerStoriesMuted(peerId, mute)
@@ -909,14 +951,14 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
chatListController?.push(exceptionController)
})
})))
-
+
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteForever, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Muted"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: Int32.max).startStandalone()
-
+
let iconColor: UIColor = .white
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
@@ -926,16 +968,16 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
-
+
c?.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil, animated: true)
}
})))
-
+
if threadId != 1 {
if canOpenClose {
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? presentationData.strings.ChatList_Context_ReopenTopic : presentationData.strings.ChatList_Context_CloseTopic, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
-
+
let _ = context.engine.peers.setForumChannelTopicClosed(id: peerId, threadId: threadId, isClosed: !threadData.isClosed).startStandalone()
})))
}
@@ -947,7 +989,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
} else if let chatListController {
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
-
+
items.append(ActionSheetTextItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
@@ -955,7 +997,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
let _ = context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).startStandalone(completed: {
})
}))
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
@@ -971,7 +1013,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
})))
}
}
-
+
if canSelect, let chatListController = chatListController as? ChatListControllerImpl {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] _, f in
@@ -979,7 +1021,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: EnginePeer.
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
})))
}
-
+
return .single(items)
}
}
@@ -996,12 +1038,12 @@ public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64,
)
|> mapToSignal { [weak parentController] peer, pinnedThreadIds -> Signal<[ContextMenuItem], NoError> in
var items: [ContextMenuItem] = []
-
+
let isPinned = pinnedThreadIds.contains(threadId)
-
+
items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
-
+
let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: threadId)
|> deliverOnMainQueue).startStandalone(error: { error in
switch error {
@@ -1021,12 +1063,12 @@ public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64,
}
})
})))
-
+
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
deletePeerChat(EnginePeer.Id(threadId))
f(.default)
})))
-
+
return .single(items)
}
}
@@ -1034,14 +1076,14 @@ 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, completion: { [weak baseController] value in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
-
+
if value <= 0 {
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: nil).startStandalone()
} else {
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: value).startStandalone()
-
+
let timeString = stringForPreciseRelativeTimestamp(strings: presentationData.strings, relativeTimestamp: Int32(Date().timeIntervalSince1970) + value, relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: presentationData.dateTimeFormat)
-
+
baseController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_mute_for", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedUntil(timeString).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
}
})
diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift
index afb7a667c8..ecf3ff9c63 100644
--- a/submodules/ChatListUI/Sources/ChatListController.swift
+++ b/submodules/ChatListUI/Sources/ChatListController.swift
@@ -66,17 +66,17 @@ import AvatarComponent
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceNode: ASDisplayNode?
-
+
let navigationController: NavigationController?
-
+
let passthroughTouches: Bool = true
-
+
init(controller: ViewController, sourceNode: ASDisplayNode?, navigationController: NavigationController?) {
self.controller = controller
self.sourceNode = sourceNode
self.navigationController = navigationController
}
-
+
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
@@ -87,76 +87,76 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
})
}
-
+
func animatedIn() {
}
}
public class ChatListControllerImpl: TelegramBaseController, ChatListController {
private var validLayout: ContainerViewLayout?
-
+
public let context: AccountContext
private let controlsHistoryPreload: Bool
private let hideNetworkActivityStatus: Bool
-
+
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
-
+
public let location: ChatListControllerLocation
public let previewing: Bool
-
+
let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable()
-
+
private var chatListDisplayNode: ChatListControllerNode {
return super.displayNode as! ChatListControllerNode
}
-
+
fileprivate private(set) var primaryContext: ChatListLocationContext?
private let primaryInfoReady = Promise()
private let mainReady = Promise()
private let storiesReady = Promise()
-
+
private var pendingSecondaryContext: ChatListLocationContext?
fileprivate private(set) var secondaryContext: ChatListLocationContext?
-
+
fileprivate var effectiveContext: ChatListLocationContext? {
return self.secondaryContext ?? self.primaryContext
}
-
+
public var effectiveLocation: ChatListControllerLocation {
return self.secondaryContext?.location ?? self.location
}
-
+
private var badgeDisposable: Disposable?
private var badgeIconDisposable: Disposable?
-
+
private var didAppear = false
private var dismissSearchOnDisappear = false
public var onDidAppear: (() -> Void)?
-
+
private var passcodeLockTooltipDisposable = MetaDisposable()
private var didShowPasscodeLockTooltipController = false
-
+
private var suggestLocalizationDisposable = MetaDisposable()
private var didSuggestLocalization = false
-
+
private let suggestAutoarchiveDisposable = MetaDisposable()
private let dismissAutoarchiveDisposable = MetaDisposable()
private var didSuggestAutoarchive = false
private var didSuggestLoginEmailSetup = false
private var didSuggestLoginPasskeySetup = false
-
+
private(set) var presentationData: PresentationData
private let presentationDataValue = Promise()
private var presentationDataDisposable: Disposable?
-
+
private let stateDisposable = MetaDisposable()
private let filterDisposable = MetaDisposable()
private let featuredFiltersDisposable = MetaDisposable()
private var processedFeaturedFilters = false
-
+
private let isReorderingTabsValue = ValuePromise(false)
-
+
private(set) var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
var hasTabs: Bool {
if let tabContainerData = self.tabContainerData {
@@ -179,40 +179,40 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}))
}
-
+
private var hasDownloads: Bool = false
private var activeDownloadsDisposable: Disposable?
private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer?
-
+
private(set) var isPremium: Bool = false
private(set) var storyPostingAvailability: StoriesConfiguration.PostingAvailability = .disabled
private var storiesPostingAvailabilityDisposable: Disposable?
private let storyPostingAvailabilityValue = ValuePromise(.disabled)
-
+
private var didSetupTabs = false
-
+
private weak var emojiStatusSelectionController: ViewController?
-
+
private var forumChannelTracker: ForumChannelTopics?
-
+
private let selectAddMemberDisposable = MetaDisposable()
private let addMemberDisposable = MetaDisposable()
private let joinForumDisposable = MetaDisposable()
private let actionDisposables = DisposableSet()
-
+
private var plainTitle: String = ""
-
+
private var powerSavingMonitoringDisposable: Disposable?
-
+
private var rawStoryArchiveSubscriptions: EngineStorySubscriptions?
private var storyArchiveSubscriptionsDisposable: Disposable?
-
+
private var rawStorySubscriptions: EngineStorySubscriptions?
private var shouldFixStorySubscriptionOrder: Bool = false
private var fixedStorySubscriptionOrder: [EnginePeer.Id] = []
private(set) var orderedStorySubscriptions: EngineStorySubscriptions?
private var displayedStoriesTooltip: Bool = false
-
+
public var hasStorySubscriptions: Bool {
if let rawStorySubscriptions = self.rawStorySubscriptions, !rawStorySubscriptions.items.isEmpty {
return true
@@ -220,45 +220,45 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}
}
-
+
private let hasPendingStoriesPromise = ValuePromise(false, ignoreRepeated: true)
public var hasPendingStories: Signal {
return self.hasPendingStoriesPromise.get()
}
-
+
private var storyProgressDisposable: Disposable?
private var storySubscriptionsDisposable: Disposable?
private var preloadStorySubscriptionsDisposable: Disposable?
private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:]
-
+
private var sharedOpenStoryProgressDisposable = MetaDisposable()
-
+
var currentTooltipUpdateTimer: Foundation.Timer?
-
+
let globalControlPanelsContext: GlobalControlPanelsContext
private(set) var globalControlPanelsContextState: GlobalControlPanelsContext.State?
private var globalControlPanelsContextStateDisposable: Disposable?
-
+
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isNodeLoaded {
self.chatListDisplayNode.effectiveContainerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition)
}
}
-
+
public init(context: AccountContext, location: ChatListControllerLocation, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool = false, previewing: Bool = false, enableDebugActions: Bool) {
self.context = context
self.controlsHistoryPreload = controlsHistoryPreload
self.hideNetworkActivityStatus = hideNetworkActivityStatus
-
+
self.location = location
self.previewing = previewing
-
+
self.presentationData = (context.sharedContext.currentPresentationData.with { $0 })
self.presentationDataValue.set(.single(self.presentationData))
-
+
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
-
+
var groupCallPanelSource: EnginePeer.Id?
var chatListNotices = false
switch self.location {
@@ -271,7 +271,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case .savedMessagesChats:
break
}
-
+
self.globalControlPanelsContext = GlobalControlPanelsContext(
context: context,
mediaPlayback: true,
@@ -279,16 +279,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
groupCalls: groupCallPanelSource,
chatListNotices: chatListNotices
)
-
+
super.init(context: context, navigationBarPresentationData: nil)
-
+
self.accessoryPanelContainer = ASDisplayNode()
-
+
self.tabBarItemContextActionType = .always
self.automaticallyControlPresentationContextLayout = false
-
+
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
-
+
let title: String
switch self.location {
case let .chatList(groupId):
@@ -304,7 +304,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case .savedMessagesChats:
title = ""
}
-
+
let primaryContext = ChatListLocationContext(
context: context,
location: self.location,
@@ -323,41 +323,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)
self.primaryContext = primaryContext
self.primaryInfoReady.set(primaryContext.ready.get())
-
+
if !previewing {
switch self.location {
case let .chatList(groupId):
if groupId == .root {
self.tabBarItem.title = self.presentationData.strings.DialogList_Title
-
+
let icon: UIImage?
if useSpecialTabBarIcons() {
icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconChats")
} else {
icon = UIImage(bundleImageName: "Chat List/Tabs/IconChats")
}
-
+
self.tabBarItem.image = icon
self.tabBarItem.selectedImage = icon
if !self.presentationData.reduceMotion {
self.tabBarItem.animationName = "TabChats"
self.tabBarItem.animationOffset = CGPoint(x: 0.0, y: UIScreenPixel)
}
-
+
self.primaryContext?.leftButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent(
content: .text(title: self.presentationData.strings.Common_Edit, isBold: false),
pressed: { [weak self] _ in
self?.editPressed()
}
)))
-
+
self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "compose", component: AnyComponent(NavigationButtonComponent(
content: .icon(imageName: "Chat List/ComposeIcon"),
pressed: { [weak self] _ in
self?.composePressed()
}
)))
-
+
//let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil)
//backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back
//self.navigationItem.backBarButtonItem = backBarButtonItem
@@ -375,7 +375,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case .savedMessagesChats:
break
}
-
+
let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back
self.navigationItem.backBarButtonItem = backBarButtonItem
@@ -386,7 +386,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
break
}
}
-
+
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.chatListDisplayNode.willScrollToTop()
@@ -397,7 +397,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
if strongSelf.chatListDisplayNode.searchDisplayController != nil {
strongSelf.deactivateSearch(animated: true)
} else {
@@ -407,7 +407,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false))
case let .known(offset):
let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.mainContainerNode.availableFilters.first?.filter
-
+
if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && strongSelf.chatListDisplayNode.inlineStackContainerNode != nil {
strongSelf.setInlineChatList(location: nil)
} else if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && !isFirstFilter {
@@ -424,7 +424,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let componentView = strongSelf.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
storyPeerListView.scrollToTop()
}
-
+
strongSelf.chatListDisplayNode.willScrollToTop()
if let inlineStackContainerNode = strongSelf.chatListDisplayNode.inlineStackContainerNode {
inlineStackContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false))
@@ -435,7 +435,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
self.badgeDisposable = (combineLatest(renderedTotalUnreadCount(accountManager: context.sharedContext.accountManager, engine: context.engine), self.presentationDataValue.get()) |> deliverOnMainQueue).startStrict(next: { [weak self] count, presentationData in
if let strongSelf = self {
if count.0 == 0 {
@@ -445,29 +445,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}).strict()
-
+
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
-
+
strongSelf.presentationData = presentationData
strongSelf.presentationDataValue.set(.single(presentationData))
-
+
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
}).strict()
-
+
if !previewing {
enum State: Equatable {
case empty(hasDownloads: Bool)
case downloading(progress: Double)
case hasUnseen
}
-
+
let entriesWithFetchStatuses = Signal<[(entry: FetchManagerEntrySummary, progress: Double)], NoError> { subscriber in
let queue = Queue()
final class StateHolder {
@@ -476,26 +476,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var isRemoved: Bool = false
var statusDisposable: Disposable?
var status: EngineMediaResource.FetchStatus?
-
+
init(entry: FetchManagerEntrySummary) {
self.entry = entry
}
-
+
deinit {
self.statusDisposable?.dispose()
}
}
-
+
let queue: Queue
-
+
var entryContexts: [FetchManagerLocationEntryId: EntryContext] = [:]
-
+
let state = Promise<[(entry: FetchManagerEntrySummary, progress: Double)]>()
-
+
init(queue: Queue) {
self.queue = queue
}
-
+
func update(engine: TelegramEngine, entries: [FetchManagerEntrySummary]) {
if entries.isEmpty {
self.entryContexts.removeAll()
@@ -508,9 +508,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
context = EntryContext(entry: entry)
self.entryContexts[entry.id] = context
}
-
+
context.entry = entry
-
+
if context.isRemoved {
context.isRemoved = false
context.status = nil
@@ -518,12 +518,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
context.statusDisposable = nil
}
}
-
+
for (_, context) in self.entryContexts {
if !entries.contains(where: { $0.id == context.entry.id }) {
context.isRemoved = true
}
-
+
if context.statusDisposable == nil {
context.statusDisposable = (engine.resources.status(resource: EngineMediaResource(context.entry.resourceReference.resource))
|> deliverOn(self.queue)).startStrict(next: { [weak self, weak context] status in
@@ -538,10 +538,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
self.notifyUpdatedIfReady()
}
-
+
func notifyUpdatedIfReady() {
var result: [(entry: FetchManagerEntrySummary, progress: Double)] = []
loop: for (_, context) in self.entryContexts {
@@ -581,19 +581,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
subscriber.putNext(state)
}))
}
-
+
return ActionDisposable {
entriesDisposable.dispose()
holderStateDisposable.dispose()
}
}
-
+
let displayRecentDownloads = context.account.postbox.tailChatListView(groupId: .root, filterPredicate: nil, count: 11, summaryComponents: ChatListEntrySummaryComponents(components: [:]))
|> map { view -> Bool in
return view.0.entries.count >= 10
}
|> distinctUntilChanged
-
+
let stateSignal: Signal = (combineLatest(queue: .mainQueue(), entriesWithFetchStatuses, recentDownloadItems(postbox: context.account.postbox), displayRecentDownloads)
|> map { entries, recentDownloadItems, displayRecentDownloads -> State in
if !entries.isEmpty && displayRecentDownloads {
@@ -628,7 +628,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
|> distinctUntilChanged
|> deliverOnMainQueue)
-
+
self.activeDownloadsDisposable = stateSignal.startStrict(next: { [weak self] state in
guard let strongSelf = self else {
return
@@ -639,7 +639,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
switch state {
case let .downloading(progress):
strongSelf.hasDownloads = true
-
+
animation = LottieAnimationComponent.AnimationItem(
name: "anim_search_downloading",
mode: .animating(loop: true)
@@ -650,12 +650,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
]
progressValue = progress
-
+
strongSelf.clearUnseenDownloadsTimer?.invalidate()
strongSelf.clearUnseenDownloadsTimer = nil
case .hasUnseen:
strongSelf.hasDownloads = true
-
+
animation = LottieAnimationComponent.AnimationItem(
name: "anim_search_downloaded",
mode: .animating(loop: false)
@@ -671,7 +671,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
]
progressValue = 1.0
-
+
if strongSelf.clearUnseenDownloadsTimer == nil {
let timeout: Double
#if DEBUG
@@ -690,15 +690,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
case let .empty(hasDownloadsValue):
strongSelf.hasDownloads = hasDownloadsValue
-
+
animation = nil
colors = [:]
progressValue = nil
-
+
strongSelf.clearUnseenDownloadsTimer?.invalidate()
strongSelf.clearUnseenDownloadsTimer = nil
}
-
+
if let animation = animation, let progressValue = progressValue {
let contentComponent = AnyComponent(ZStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieAnimationComponent(
@@ -713,7 +713,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
value: progressValue
)))
]))
-
+
if let navigationBarView = strongSelf.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarView.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
content: contentComponent,
@@ -732,26 +732,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
if enableDebugActions {
self.tabBarItemDebugTapAction = {
preconditionFailure("debug tap")
}
}
-
+
if case .chatList(.root) = self.location {
self.chatListDisplayNode.mainContainerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in
guard let strongSelf = self else {
return
}
-
+
if let navigationBarView = strongSelf.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let headerPanelsView = navigationBarView.headerPanels as? HeaderPanelContainerComponent.View, let tabsView = headerPanelsView.tabs as? HorizontalTabsComponent.View {
tabsView.updateTabSwitchFraction(fraction: fraction, isDragging: strongSelf.chatListDisplayNode.mainContainerNode.isSwitchingCurrentItemFilterByDragging, transition: ComponentTransition(transition))
}
}
self.reloadFilters()
}
-
+
self.storiesPostingAvailabilityDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration))
|> map { view -> AppConfiguration in
let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
@@ -769,11 +769,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.storyPostingAvailabilityValue.set(postingAvailability)
}
})
-
+
self.updateNavigationMetadata()
self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate)
-
+
self.globalControlPanelsContextStateDisposable = (self.globalControlPanelsContext.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
@@ -787,7 +787,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
deinit {
self.openMessageFromSearchDisposable.dispose()
self.badgeDisposable?.dispose()
@@ -817,12 +817,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
self.globalControlPanelsContextStateDisposable?.dispose()
}
-
+
private func updateNavigationMetadata() {
guard let currentContext = self.secondaryContext ?? self.primaryContext else {
return
}
-
+
switch currentContext.location {
case .chatList:
self.navigationBar?.userInfo = nil
@@ -847,14 +847,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
func findTitleView() -> ChatListTitleView? {
guard let componentView = self.chatListHeaderView() else {
return nil
}
return componentView.findTitleView()
}
-
+
private var previousEmojiSetupTimestamp: Double?
func openStatusSetup(sourceView: UIView) {
let currentTimestamp = CACurrentMediaTime()
@@ -862,7 +862,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
self.previousEmojiSetupTimestamp = currentTimestamp
-
+
self.emojiStatusSelectionController?.dismiss()
var selectedItems = Set()
var topStatusTitle = self.presentationData.strings.PeerStatusSetup_NoTimerTitle
@@ -870,7 +870,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let emojiStatus = self.chatListHeaderView()?.emojiStatus() {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId))
currentSelection = emojiStatus.fileId
-
+
if let timestamp = emojiStatus.expirationDate {
topStatusTitle = peerStatusExpirationString(statusTimestamp: timestamp, relativeTo: Int32(Date().timeIntervalSince1970), strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
}
@@ -904,23 +904,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.emojiStatusSelectionController = controller
self.present(controller, in: .window(.root))
}
-
+
func allowAutomaticOrder() {
if !self.shouldFixStorySubscriptionOrder {
return
}
-
+
self.shouldFixStorySubscriptionOrder = false
self.fixedStorySubscriptionOrder = self.rawStorySubscriptions?.items.map(\.peer.id) ?? []
if self.orderedStorySubscriptions != self.rawStorySubscriptions {
self.orderedStorySubscriptions = self.rawStorySubscriptions
-
+
// important not to cause a loop
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
-
+
self.chatListDisplayNode.requestNavigationBarLayout(transition: ComponentTransition.immediate.withUserData(ChatListNavigationBar.AnimationHint(
disableStoriesAnimations: false,
crossfadeStoryPeers: true
@@ -928,14 +928,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
private func updateThemeAndStrings() {
if case .chatList(.root) = self.location {
self.tabBarItem.title = self.presentationData.strings.DialogList_Title
let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil)
backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back
self.navigationItem.backBarButtonItem = backBarButtonItem
-
+
if !self.presentationData.reduceMotion {
self.tabBarItem.animationName = "TabChats"
} else {
@@ -946,17 +946,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back
self.navigationItem.backBarButtonItem = backBarButtonItem
}
-
+
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData), transition: .immediate)
-
+
if self.isNodeLoaded {
self.chatListDisplayNode.updatePresentationData(self.presentationData)
}
-
+
self.requestLayout(transition: .immediate)
}
-
+
func tabContextGesture(id: Int32?, sourceNode: ContextExtractedContentContainingNode?, sourceView: ContextExtractedContentContainingView?, gesture: ContextGesture?, keepInPlace: Bool, isDisabled: Bool) {
let context = self.context
let filterPeersAreMuted: Signal<(areMuted: Bool, peerIds: [EnginePeer.Id])?, NoError> = self.context.engine.peers.currentChatListFilters()
@@ -968,7 +968,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard case let .filter(_, _, _, data) = filter else {
return .single(nil)
}
-
+
let filterPredicate: ChatListFilterPredicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
return context.engine.peers.getChatListPeers(filterPredicate: filterPredicate)
|> mapToSignal { peers -> Signal<(areMuted: Bool, peerIds: [EnginePeer.Id])?, NoError> in
@@ -1007,7 +1007,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let _ = combineLatest(
queue: Queue.mainQueue(),
self.context.engine.peers.currentChatListFilters(),
@@ -1062,7 +1062,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
})))
-
+
if let _ = filters.first(where: { $0.id == id }) {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.ChatList_AddChatsToFolder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
@@ -1071,7 +1071,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
if isDisabled {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
@@ -1102,10 +1102,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if filter.id == id, case let .filter(_, _, _, data) = filter {
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
-
+
let limit = limits.maxFolderChatsCount
let premiumLimit = premiumLimits.maxFolderChatsCount
-
+
if data.includePeers.peers.count >= premiumLimit {
let controller = PremiumLimitScreen(context: self.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {
return true
@@ -1127,7 +1127,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
f(.dismissWithoutContent)
return
}
-
+
let _ = (self.context.engine.peers.currentChatListFilters()
|> deliverOnMainQueue).startStandalone(next: { [weak self] filters in
guard let self else {
@@ -1152,7 +1152,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
})))
-
+
if let filterEntries = self.tabContainerData?.0 {
for filter in filterEntries {
if case let .filter(filterId, _, unread) = filter, filterId == id {
@@ -1168,7 +1168,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
for filter in filters {
if filter.id == filterId, case let .filter(_, title, _, data) = filter {
if let filterPeersAreMuted, filterPeersAreMuted.peerIds.count <= 200 {
@@ -1177,17 +1177,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}, action: { [weak self] c, f in
c?.dismiss(completion: {
})
-
+
guard let self else {
return
}
-
+
let _ = (self.context.engine.peers.updateMultiplePeerMuteSettings(peerIds: filterPeersAreMuted.peerIds, muted: !filterPeersAreMuted.areMuted)
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
guard let self else {
return
}
-
+
let iconColor: UIColor = .white
let overlayController: UndoOverlayController
if !filterPeersAreMuted.areMuted {
@@ -1199,7 +1199,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
ChatTextInputAttributes.bold: true
]), at: folderNameRange.location)
}
-
+
overlayController = UndoOverlayController(presentationData: self.presentationData, content: .universalWithEntities(context: self.context, animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
@@ -1216,7 +1216,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
ChatTextInputAttributes.bold: true
]), at: folderNameRange.location)
}
-
+
overlayController = UndoOverlayController(presentationData: self.presentationData, content: .universalWithEntities(context: self.context, animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
@@ -1229,7 +1229,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.ChatList_ContextMenuShare, textColor: .primary, badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
@@ -1242,16 +1242,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
break
}
}
-
+
break
}
}
}
-
+
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.ChatList_RemoveFolder, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] c, f in
@@ -1275,7 +1275,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
if filters.count > 1 {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.ChatList_ReorderTabs, icon: { theme in
@@ -1285,10 +1285,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.chatListDisplayNode.isReorderingFilters = true
self.isReorderingTabsValue.set(true)
-
+
(self.parent as? TabBarController)?.updateIsTabBarEnabled(false, transition: .animated(duration: 0.2, curve: .easeInOut))
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
@@ -1296,7 +1296,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
if let sourceNode {
let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: sourceView, keepInPlace: keepInPlace)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
@@ -1306,26 +1306,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
override public func loadDisplayNode() {
self.displayNode = ChatListControllerNode(context: self.context, location: self.location, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, controller: self)
-
+
self.chatListDisplayNode.navigationBar = self.navigationBar
-
+
self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch(animated: true)
}
-
+
self.chatListDisplayNode.mainContainerNode.activateSearch = { [weak self] in
self?.activateSearch()
}
-
+
self.chatListDisplayNode.mainContainerNode.presentAlert = { [weak self] text in
if let strongSelf = self {
self?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
-
+
self.chatListDisplayNode.mainContainerNode.present = { [weak self] c in
if let strongSelf = self {
if c is UndoOverlayController {
@@ -1336,27 +1336,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
self.chatListDisplayNode.mainContainerNode.push = { [weak self] c in
if let strongSelf = self {
strongSelf.push(c)
}
}
-
+
self.chatListDisplayNode.mainContainerNode.toggleArchivedFolderHiddenByDefault = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleArchivedFolderHiddenByDefault()
}
-
+
self.chatListDisplayNode.mainContainerNode.hidePsa = { [weak self] peerId in
guard let strongSelf = self else {
return
}
strongSelf.hidePsa(peerId)
}
-
+
self.chatListDisplayNode.mainContainerNode.deletePeerChat = { [weak self] peerId, joined in
guard let strongSelf = self else {
return
@@ -1387,22 +1387,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
strongSelf.setPeerThreadHidden(peerId: peerId, threadId: threadId, isHidden: isHidden)
}
-
+
self.chatListDisplayNode.mainContainerNode.peerSelected = { [weak self] peer, threadId, animated, activateInput, promoInfo in
Task { @MainActor [weak self] in
guard let self else {
return
}
-
+
let subject: ChatControllerSubject? = nil
-
+
var forumSourcePeer: Signal = .single(nil)
if case let .savedMessagesChats(peerId) = self.location, peerId != self.context.account.peerId {
forumSourcePeer = self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
}
-
+
let _ = (combineLatest(queue: .mainQueue(),
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.CachedData(id: peer.id)),
forumSourcePeer
@@ -1414,21 +1414,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let navigationController = self.navigationController as? NavigationController else {
return
}
-
+
var peer = peer
var threadId = threadId
if let forumSourcePeer {
threadId = peer.id.toInt64()
peer = forumSourcePeer
}
-
+
var scrollToEndIfExists = false
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
scrollToEndIfExists = true
}
-
+
var openAsInlineForum = true
-
+
if case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) {
openAsInlineForum = false
} else if case let .channel(channel) = peer, channel.flags.contains(.displayForumAsTabs) {
@@ -1438,7 +1438,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
openAsInlineForum = false
}
}
-
+
if openAsInlineForum, case let .channel(channel) = peer, channel.isForum, threadId == nil {
self.chatListDisplayNode.clearHighlightAnimated(true)
if self.chatListDisplayNode.inlineStackContainerNode?.location == .forum(peerId: channel.id) {
@@ -1448,7 +1448,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return
}
-
+
if case let .channel(channel) = peer, channel.isForumOrMonoForum, let threadId {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
@@ -1471,7 +1471,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
subject: subject,
keepStack: .always
))
-
+
self.chatListDisplayNode.clearHighlightAnimated(true)
} else {
var navigationAnimationOptions: NavigationAnimationOptions = []
@@ -1482,10 +1482,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
navigationAnimationOptions = .removeOnMasterDetails
}
}
-
+
let chatLocation: NavigateToChatControllerParams.Location
chatLocation = .peer(peer)
-
+
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: self.context,
@@ -1501,7 +1501,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
self.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
-
+
if let promoInfo = promoInfo {
switch promoInfo {
case .proxy:
@@ -1529,7 +1529,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else if let string = self.presentationData.strings.secondaryComponent?.dict[key] {
text = string
}
-
+
controller.displayPromoAnnouncement(text: text)
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: self.context.sharedContext.accountManager, peerId: peer.id).startStandalone()
}
@@ -1541,12 +1541,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
}
-
+
self.chatListDisplayNode.mainContainerNode.groupSelected = { [weak self] groupId in
guard let self else {
return
}
-
+
let _ = self.context.engine.privacy.updateGlobalPrivacySettings().startStandalone()
let _ = (combineLatest(
ApplicationSpecificNotice.displayChatListArchiveTooltip(accountManager: self.context.sharedContext.accountManager),
@@ -1559,26 +1559,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
-
+
if let navigationController = self.navigationController as? NavigationController {
let chatListController = ChatListControllerImpl(context: self.context, location: .chatList(groupId: groupId), controlsHistoryPreload: false, enableDebugActions: false)
chatListController.navigationPresentation = .master
navigationController.pushViewController(chatListController)
}
-
+
if !didDisplayTip, chatListHead.items.count < 10 {
#if DEBUG
#else
let _ = ApplicationSpecificNotice.setDisplayChatListArchiveTooltip(accountManager: self.context.sharedContext.accountManager).startStandalone()
#endif
-
+
self.push(ArchiveInfoScreen(context: self.context, settings: settings))
}
})
}
-
+
self.chatListDisplayNode.mainContainerNode.updatePeerGrouping = { [weak self] peerId, group in
guard let strongSelf = self else {
return
@@ -1595,21 +1595,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
}
-
+
self.chatListDisplayNode.mainContainerNode.openBirthdaySetup = { [weak self] in
guard let self else {
return
}
self.openBirthdaySetup()
}
-
+
self.chatListDisplayNode.mainContainerNode.openStarsTopup = { [weak self] amount in
guard let self else {
return
}
self.openStarsTopup(amount: amount)
}
-
+
self.chatListDisplayNode.mainContainerNode.openWebApp = { [weak self] user in
guard let self else {
return
@@ -1630,7 +1630,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
verifyAgeCompletion: nil
)
}
-
+
self.chatListDisplayNode.mainContainerNode.openAccountFreezeInfo = { [weak self] in
guard let self else {
return
@@ -1638,7 +1638,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)
self.push(controller)
}
-
+
self.chatListDisplayNode.mainContainerNode.openPhotoSetup = { [weak self] in
guard let self else {
return
@@ -1648,7 +1648,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return nil
}
-
+
let toastScreen = AvatarUploadToastScreen(
context: self.context,
image: image,
@@ -1677,7 +1677,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
)
-
+
if let navigationController = self.navigationController as? NavigationController {
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 is TabBarController }) {
@@ -1689,13 +1689,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
self.push(toastScreen)
}
-
+
return toastScreen.targetAvatarView
})
}
}
-
-
+
+
self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in
guard let self else {
return
@@ -1708,7 +1708,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: context.sharedContext.currentPresentationData.with({$0}), navigationController: self.navigationController as? NavigationController, dismissInput: {})
}
-
+
self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, threadId, messageId, deactivateOnAction in
if let strongSelf = self {
strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer)
@@ -1738,7 +1738,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
}
}
-
+
self.chatListDisplayNode.requestOpenPeerFromSearch = { [weak self] peer, threadId, dismissSearch in
if let strongSelf = self {
let storedPeer = strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) |> map { _ -> Void in return Void() }
@@ -1769,23 +1769,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
}
}
-
+
self.chatListDisplayNode.dismissSearch = { [weak self] in
if let self {
self.deactivateSearch(animated: true)
}
}
-
+
self.chatListDisplayNode.requestOpenRecentPeerOptions = { [weak self] peer in
if let strongSelf = self {
strongSelf.view.window?.endEditing(true)
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
if let strongSelf = self {
let _ = strongSelf.context.engine.peers.removeRecentPeer(peerId: peer.id).startStandalone()
}
@@ -1800,7 +1800,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.present(actionSheet, in: .window(.root))
}
}
-
+
self.chatListDisplayNode.requestAddContact = { [weak self] phoneNumber in
if let strongSelf = self {
strongSelf.view.endEditing(true)
@@ -1813,7 +1813,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
}
-
+
self.chatListDisplayNode.dismissSelfIfCompletedPresentation = { [weak self] in
guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else {
return
@@ -1823,7 +1823,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
navigationController.filterController(strongSelf, animated: true)
}
-
+
self.chatListDisplayNode.emptyListAction = { [weak self] _ in
guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else {
return
@@ -1835,10 +1835,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let context = strongSelf.context
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
controller.navigationPresentation = .modal
-
+
controller.completion = { [weak controller] title, fileId, iconColor, _ in
controller?.isInProgress = true
-
+
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId)
|> deliverOnMainQueue).startStandalone(next: { topicId in
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, scrollToEndIfExists: false, keepStack: .never, animated: true).startStandalone()
@@ -1852,24 +1852,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
self.chatListDisplayNode.cancelEditing = { [weak self] in
guard let strongSelf = self else {
return
}
let _ = strongSelf.reorderingDonePressed()
}
-
+
self.chatListDisplayNode.toolbarActionSelected = { [weak self] action in
self?.toolbarActionSelected(action: action)
}
-
+
self.chatListDisplayNode.mainContainerNode.activateChatPreview = { [weak self] item, threadId, node, gesture, location in
guard let strongSelf = self else {
gesture?.cancel()
return
}
-
+
var joined = false
if case let .peer(peerData) = item.content, let message = peerData.messages.first {
for media in message.media {
@@ -1878,7 +1878,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
switch item.content {
case .loading:
break
@@ -1891,7 +1891,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let peer = peerData.peer
let threadInfo = peerData.threadInfo
let promoInfo = peerData.promoInfo
-
+
switch item.index {
case .chatList:
if case let .channel(channel) = peer.peer, (channel.isForum || (channel.isMonoForum && threadId != nil)) {
@@ -1902,7 +1902,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
-
+
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 {
@@ -1919,7 +1919,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
source = .controller(ContextControllerContentSourceImpl(controller: peerInfoController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
-
+
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)
}
@@ -1937,10 +1937,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
-
+
let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, 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)
-
+
dismissPreviewingImpl = { [weak self, weak contextController] animateIn in
if let self, let contextController {
if animateIn {
@@ -1971,13 +1971,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
-
+
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)
}
}
}
-
+
self.chatListDisplayNode.mainContainerNode.openStories = { [weak self] subject, itemNode in
guard let self else {
return
@@ -1985,11 +1985,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let itemNode = itemNode as? ChatListItemNode else {
return
}
-
+
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() {
storyPeerListView.cancelLoadingItem()
}
-
+
switch subject {
case .archive:
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable)
@@ -1997,13 +1997,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable)
}
}
-
+
self.chatListDisplayNode.peerContextAction = { [weak self] peer, source, node, gesture, location in
guard let strongSelf = self else {
gesture?.cancel()
return
}
-
+
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
@@ -2022,12 +2022,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
chatController.canReadHistory.set(false)
contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
-
+
let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, 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)
}
}
-
+
if case .chatList(.root) = self.location {
self.ready.set(combineLatest([self.mainReady.get(), self.storiesReady.get()])
|> map { values -> Bool in
@@ -2040,22 +2040,34 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.primaryInfoReady.get(),
self.storiesReady.get()
]
-
+
if case .chatList(.archive) = self.location {
//signals.append(self.mainReady.get())
} else {
self.storiesReady.set(.single(true))
}
-
+
self.ready.set(combineLatest(signals)
|> map { values -> Bool in
return !values.contains(where: { !$0 })
}
|> filter { $0 })
}
-
+
self.displayNodeDidLoad()
-
+
+ // WinterGram: two-finger swipe on the chat list to quickly cycle between accounts.
+ if case .chatList(.root) = self.location {
+ let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.winterGramQuickSwitchGesture(_:)))
+ leftSwipe.direction = .left
+ leftSwipe.numberOfTouchesRequired = 2
+ self.displayNode.view.addGestureRecognizer(leftSwipe)
+ let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.winterGramQuickSwitchGesture(_:)))
+ rightSwipe.direction = .right
+ rightSwipe.numberOfTouchesRequired = 2
+ self.displayNode.view.addGestureRecognizer(rightSwipe)
+ }
+
if case .chatList = self.location {
let automaticDownloadNetworkType = context.account.networkType
|> map { type -> MediaAutoDownloadNetworkType in
@@ -2067,7 +2079,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
|> distinctUntilChanged
-
+
let preferHighQualityStories: Signal = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
@@ -2083,7 +2095,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return setting && isPremium
}
|> distinctUntilChanged
-
+
self.preloadStorySubscriptionsDisposable = (combineLatest(queue: .mainQueue(),
self.context.engine.messages.preloadStorySubscriptions(isHidden: self.location == .chatList(groupId: .archive), preferHighQuality: preferHighQualityStories),
self.context.sharedContext.automaticMediaDownloadSettings,
@@ -2093,17 +2105,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
var autodownloadEnabled = true
if !shouldDownloadMediaAutomatically(settings: automaticMediaDownloadSettings, peerType: .contact, networkType: automaticDownloadNetworkType, authorPeerId: nil, contactsPeerIds: [], media: nil, isStory: true) {
autodownloadEnabled = false
}
-
+
var resources = resources
if !autodownloadEnabled {
resources.removeAll()
}
-
+
var validIds: [MediaId] = []
for (_, info) in resources.sorted(by: { $0.value.priority < $1.value.priority }) {
if let mediaId = info.media.id {
@@ -2113,7 +2125,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
var removeIds: [MediaId] = []
for (id, disposable) in self.preloadStoryResourceDisposables {
if !validIds.contains(id) {
@@ -2125,7 +2137,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.preloadStoryResourceDisposables.removeValue(forKey: id)
}
})
-
+
if self.previewing {
self.storiesReady.set(.single(true))
} else {
@@ -2134,7 +2146,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.rawStorySubscriptions = rawStorySubscriptions
var items: [EngineStorySubscriptions.Item] = []
if self.shouldFixStorySubscriptionOrder {
@@ -2155,24 +2167,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
hasMoreToken: rawStorySubscriptions.hasMoreToken
)
self.fixedStorySubscriptionOrder = items.map(\.peer.id)
-
+
let transition: ContainedViewLayoutTransition
if self.didAppear {
transition = .animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
-
+
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
self.requestLayout(transition: transition)
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
-
+
if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) {
self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded()
}
-
+
self.storiesReady.set(.single(true))
-
+
Queue.mainQueue().after(1.0, { [weak self] in
guard let self else {
return
@@ -2187,16 +2199,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
self.updateStoryUploadProgress(progress)
})
-
+
if case .chatList(.root) = self.location {
self.storyArchiveSubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: true)
|> deliverOnMainQueue).startStrict(next: { [weak self] rawStoryArchiveSubscriptions in
guard let self else {
return
}
-
+
self.rawStoryArchiveSubscriptions = rawStoryArchiveSubscriptions
-
+
let archiveStoryState: ChatListNodeState.StoryState?
if rawStoryArchiveSubscriptions.items.isEmpty {
archiveStoryState = nil
@@ -2218,31 +2230,31 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
hasUnseenCloseFriends: hasUnseenCloseFriends
)
}
-
+
self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
var chatListState = chatListState
-
+
chatListState.archiveStoryState = archiveStoryState
-
+
return chatListState
}
-
+
self.storiesReady.set(.single(true))
-
+
Queue.mainQueue().after(1.0, { [weak self] in
guard let self else {
return
}
self.maybeDisplayStoryTooltip()
})
-
+
self.hasPendingStoriesPromise.set(rawStoryArchiveSubscriptions.accountItem?.hasPending ?? false)
})
}
}
}
}
-
+
private weak var storyTooltip: TooltipScreen?
fileprivate func maybeDisplayStoryTooltip() {
let content = self.updateHeaderContent()
@@ -2258,7 +2270,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if self.displayedStoriesTooltip {
return
}
-
+
if case .chatList(groupId: .root) = self.location, let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty {
let _ = (ApplicationSpecificNotice.displayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] didDisplay in
@@ -2268,13 +2280,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if didDisplay {
return
}
-
+
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, !navigationBarView.storiesUnlocked, !self.displayedStoriesTooltip {
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView(), let (anchorView, anchorRect) = storyPeerListView.anchorForTooltip() {
self.displayedStoriesTooltip = true
-
+
let absoluteFrame = anchorView.convert(anchorRect, to: self.view)
-
+
let itemList = orderedStorySubscriptions.items.prefix(3).map(\.peer.compactDisplayTitle)
var itemListString: String = itemList.joined(separator: ", ")
if #available(iOS 13.0, *) {
@@ -2284,9 +2296,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
itemListString = value
}
}
-
+
let text: String = self.presentationData.strings.ChatList_StoryFeedTooltipUsers(itemListString).string
-
+
let tooltipScreen = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
@@ -2299,7 +2311,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)
self.present(tooltipScreen, in: .current)
self.storyTooltip = tooltipScreen
-
+
#if !DEBUG
let _ = ApplicationSpecificNotice.setDisplayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager).startStandalone()
#endif
@@ -2308,20 +2320,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
}
-
+
public override func displayNodeDidLoad() {
super.displayNodeDidLoad()
-
+
Queue.mainQueue().after(1.0) {
self.context.prefetchManager?.prepareNextGreetingSticker()
}
}
-
+
public static var sharedPreviousPowerSavingEnabled: Bool?
-
+
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
-
+
if self.powerSavingMonitoringDisposable == nil {
self.powerSavingMonitoringDisposable = (self.context.sharedContext.automaticMediaDownloadSettings
|> mapToSignal { settings -> Signal in
@@ -2332,20 +2344,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
var previousValueValue: Bool?
-
+
previousValueValue = ChatListControllerImpl.sharedPreviousPowerSavingEnabled
ChatListControllerImpl.sharedPreviousPowerSavingEnabled = isPowerSavingEnabled
-
+
/*#if DEBUG
previousValueValue = false
#endif*/
-
+
if isPowerSavingEnabled != previousValueValue && previousValueValue != nil && isPowerSavingEnabled {
let batteryLevel = UIDevice.current.batteryLevel
if batteryLevel > 0.0 && self.view.window != nil {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let batteryPercentage = Int(batteryLevel * 100.0)
-
+
self.dismissAllUndoControllers()
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "lowbattery_30", scale: 1.0, colors: [:], title: presentationData.strings.PowerSaving_AlertEnabledTitle, text: presentationData.strings.PowerSaving_AlertEnabledText("\(batteryPercentage)").string, customUndoText: presentationData.strings.PowerSaving_AlertEnabledAction, timeout: 5.0), elevatedLayout: false, action: { [weak self] action in
if case .undo = action, let self {
@@ -2361,11 +2373,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
self.didAppear = true
-
+
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
-
+
self.chatListDisplayNode.mainContainerNode.didBeginSelectingChats = { [weak self] in
guard let strongSelf = self else {
return
@@ -2385,7 +2397,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
self.chatListDisplayNode.mainContainerNode.displayFilterLimit = { [weak self] in
guard let strongSelf = self else {
return
@@ -2402,11 +2414,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
strongSelf.push(controller)
}
-
+
guard case .chatList(.root) = self.location else {
if !self.didSuggestLocalization {
self.didSuggestLocalization = true
-
+
let _ = (self.chatListDisplayNode.mainContainerNode.ready
|> filter { $0 }
|> take(1)
@@ -2417,10 +2429,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.onDidAppear?()
})
}
-
+
return
}
-
+
#if true && DEBUG
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { [weak self] in
guard let strongSelf = self else {
@@ -2440,7 +2452,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let hasPasscode = passcodeView.data.isLockable
if hasPasscode {
let _ = ApplicationSpecificNotice.setPasscodeLockTips(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
-
+
let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.DialogList_PasscodeLockHelp), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true)
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in
if let self, let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), let lockViewFrame = storyPeerListView.lockViewFrame() {
@@ -2456,14 +2468,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}))
}
-
+
if !self.didSuggestLocalization {
self.didSuggestLocalization = true
-
+
let context = self.context
-
+
let suggestedLocalization = self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SuggestedLocalization())
-
+
let signal = combineLatest(
self.context.sharedContext.accountManager.transaction { transaction -> String in
let languageCode: String
@@ -2491,7 +2503,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return (value.0, suggestedLocalization)
})
})
-
+
self.suggestLocalizationDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] suggestedLocalization in
guard let strongSelf = self, let (currentLanguageCode, suggestedLocalization) = suggestedLocalization else {
return
@@ -2506,21 +2518,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
_ = strongSelf.context.engine.localization.markSuggestedLocalizationAsSeenInteractively(languageCode: suggestedLocalization.languageCode).startStandalone()
}
}))
-
+
self.suggestAutoarchiveDisposable.set((self.context.engine.notices.getServerProvidedSuggestions()
|> deliverOnMainQueue).startStrict(next: { [weak self] values in
guard let strongSelf = self else {
return
}
-
+
let context = strongSelf.context
if values.contains(.setupLoginEmail) || values.contains(.setupLoginEmailBlocking) {
if strongSelf.didSuggestLoginEmailSetup {
return
}
-
+
strongSelf.didSuggestLoginEmailSetup = true
-
+
let _ = (context.engine.notices.getServerProvidedSuggestions(reload: true)
|> deliverOnMainQueue).start(next: { [weak self] currentValues in
guard let strongSelf = self, currentValues.contains(.setupLoginEmail) || currentValues.contains(.setupLoginEmailBlocking) else {
@@ -2545,29 +2557,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
return
}
-
+
if values.contains(.setupPasskey) {
if strongSelf.didSuggestLoginPasskeySetup {
return
}
strongSelf.didSuggestLoginPasskeySetup = true
-
+
let _ = (context.engine.notices.getServerProvidedSuggestions(reload: true)
|> deliverOnMainQueue).start(next: { [weak strongSelf] currentValues in
guard let strongSelf, currentValues.contains(.setupPasskey) else {
return
}
-
+
Task { @MainActor [weak strongSelf] in
guard let strongSelf else {
return
}
-
+
let passkeysData = await strongSelf.context.engine.auth.passkeysData().get()
if !passkeysData.isEmpty {
return
}
-
+
if let navigationController = strongSelf.navigationController as? NavigationController {
let controller = strongSelf.context.sharedContext.makePasskeySetupController(context: strongSelf.context, displaySkip: true, navigationController: navigationController, completion: {
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupPasskey.id).startStandalone()
@@ -2578,10 +2590,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
})
-
+
return
}
-
+
if strongSelf.didSuggestAutoarchive {
return
}
@@ -2605,7 +2617,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
], actionLayout: .vertical, parseMarkdown: true), in: .window(.root))
}))
-
+
Queue.mainQueue().after(1.0, {
let _ = (
self.context.engine.data.get(
@@ -2624,11 +2636,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
guard let value = value else {
return
}
-
+
let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(strongSelf.context.engine), mode: .intro(.init(
title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_Title,
text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_Text,
@@ -2640,7 +2652,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self, let controller = controller else {
return true
}
-
+
controller.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionCancel, action: {
}),
@@ -2651,15 +2663,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
controller?.dismiss()
})
], parseMarkdown: true), in: .window(.root))
-
+
return false
}
strongSelf.push(controller)
-
+
let _ = value
})
})
-
+
Queue.mainQueue().after(2.0, { [weak self] in
guard let self else {
return
@@ -2676,7 +2688,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let rightButtonView = componentView.rightButtonViews["compose"] {
let absoluteFrame = rightButtonView.convert(rightButtonView.bounds, to: self.view)
let text: String = self.presentationData.strings.ChatList_EmptyListTooltip
-
+
let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0))
self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
guard let self else {
@@ -2688,15 +2700,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
})
-
+
self.onDidAppear?()
}
-
+
self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
guard let strongSelf = self else {
return
}
-
+
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
switch controller.content {
@@ -2711,7 +2723,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return true
})
}
-
+
if !self.processedFeaturedFilters {
let initializedFeatured = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.chatListFiltersFeaturedState))
|> mapToSignal { view -> Signal in
@@ -2722,7 +2734,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
|> take(1)
-
+
let initializedFilters = self.context.engine.peers.updatedChatListFiltersInfo()
|> mapToSignal { (filters, isInitialized) -> Signal in
if isInitialized {
@@ -2732,7 +2744,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
|> take(1)
-
+
self.featuredFiltersDisposable.set((
combineLatest(initializedFeatured, initializedFilters)
|> take(1)
@@ -2742,7 +2754,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
strongSelf.processedFeaturedFilters = true
if hasFeatured {
if let _ = strongSelf.validLayout, let _ = strongSelf.parent as? TabBarController {
@@ -2754,7 +2766,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if count >= 2 {
return
}
-
+
let absoluteFrame = sourceFrame
let text: String
if hasFilters {
@@ -2764,9 +2776,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders
}
-
+
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize())
-
+
parentController.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), icon: .animation(name: "ChatListFoldersTooltip", delay: 0.6, tintColor: nil), location: .point(location, .bottom), shouldDismissOnTouch: { point, _ in
guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else {
return .dismiss(consume: false)
@@ -2782,7 +2794,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
}
}
-
+
func dismissAllUndoControllers() {
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
@@ -2790,40 +2802,40 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return true
})
-
+
if let emojiStatusSelectionController = self.emojiStatusSelectionController {
self.emojiStatusSelectionController = nil
emojiStatusSelectionController.dismiss()
}
}
-
+
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
-
+
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
-
+
self.dismissAllUndoControllers()
-
+
self.featuredFiltersDisposable.set(nil)
}
-
+
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
-
+
if self.dismissSearchOnDisappear {
self.dismissSearchOnDisappear = false
self.deactivateSearch(animated: false)
}
-
+
self.chatListDisplayNode.clearHighlightAnimated(true)
-
+
self.sharedOpenStoryProgressDisposable.set(nil)
-
+
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() {
storyPeerListView.cancelLoadingItem()
}
}
-
+
func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) {
var primaryContent: ChatListHeaderComponent.Content?
if let primaryContext = self.primaryContext {
@@ -2867,14 +2879,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
)
}
-
+
return (primaryContent, secondaryContent)
}
-
+
override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.updateNavigationBarLayout(layout, transition: transition)
}
-
+
func chatListHeaderView() -> ChatListHeaderComponent.View? {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if let componentView = navigationBarView.headerContent.view as? ChatListHeaderComponent.View {
@@ -2883,7 +2895,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return nil
}
-
+
private weak var storyCameraTooltip: TooltipScreen?
fileprivate func openStoryCamera(fromList: Bool, gesturePullOffset: CGFloat? = nil) {
guard !self.context.isFrozen else {
@@ -2891,17 +2903,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(controller)
return
}
-
+
var reachedCountLimit = false
var premiumNeeded = false
var hasActiveCall = false
var hasActiveGroupCall = false
var hasLiveStream = false
-
+
if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), storyPeerListView.isLiveStreaming {
hasLiveStream = true
}
-
+
let storiesCountLimit = self.context.userLimits.maxExpiringStoriesCount
var storiesCount = 0
if let rawStorySubscriptions = self.rawStorySubscriptions, let accountItem = rawStorySubscriptions.accountItem {
@@ -2910,7 +2922,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
reachedCountLimit = true
}
}
-
+
switch self.storyPostingAvailability {
case .premium:
if !self.isPremium {
@@ -2921,7 +2933,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
default:
break
}
-
+
if let callManager = self.context.sharedContext.callManager {
if callManager.hasActiveGroupCall {
hasActiveGroupCall = true
@@ -2929,7 +2941,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
hasActiveCall = true
}
}
-
+
if !hasLiveStream && reachedCountLimit {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
@@ -2946,7 +2958,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return
}
-
+
if !hasLiveStream && (premiumNeeded || hasActiveCall || hasActiveGroupCall) {
if let storyCameraTooltip = self.storyCameraTooltip {
self.storyCameraTooltip = nil
@@ -2970,7 +2982,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let sourceFrame {
let context = self.context
let location = CGRect(origin: CGPoint(x: sourceFrame.midX, y: sourceFrame.maxY), size: CGSize())
-
+
let text: String
if premiumNeeded {
text = self.presentationData.strings.StoryFeed_TooltipPremiumPostingLimited
@@ -2984,7 +2996,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
text = ""
}
-
+
let tooltipController = TooltipScreen(
context: context,
account: context.account,
@@ -3009,7 +3021,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return
}
-
+
var cameraTransitionIn: StoryCameraTransitionIn?
if let componentView = self.chatListHeaderView() {
if fromList {
@@ -3032,13 +3044,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: hasLiveStream, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
coordinator?.animateIn()
}
}
-
+
func displayContinueLiveStream() {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_AlertResumeLiveStreamTitle, text: self.presentationData.strings.ChatList_AlertResumeLiveStreamText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
@@ -3051,7 +3063,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
]), in: .window(.root))
}
-
+
public func storyCameraTransitionOut() -> (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut? {
return { [weak self] target, isArchived in
guard let self, let target else {
@@ -3067,7 +3079,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case .botPreview:
peerId = nil
}
-
+
if let peerId, let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
@@ -3079,20 +3091,40 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
}
-
+
+ // WinterGram: cycle to the next/previous account on a two-finger swipe.
+ @objc private func winterGramQuickSwitchGesture(_ recognizer: UISwipeGestureRecognizer) {
+ guard recognizer.state == .ended else {
+ return
+ }
+ let forward = recognizer.direction == .left
+ let _ = (self.context.sharedContext.activeAccountsWithInfo
+ |> take(1)
+ |> deliverOnMainQueue).startStandalone(next: { [weak self] _, accounts in
+ guard let self else {
+ return
+ }
+ guard accounts.count > 1, let currentIndex = accounts.firstIndex(where: { $0.account.id == self.context.account.id }) else {
+ return
+ }
+ let nextIndex = forward ? (currentIndex + 1) % accounts.count : (currentIndex - 1 + accounts.count) % accounts.count
+ self.context.sharedContext.switchToAccount(id: accounts[nextIndex].account.id, fromSettingsController: nil, withChatListController: nil)
+ })
+ }
+
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
-
+
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
-
+
self.validLayout = layout
-
+
self.updateLayout(layout: layout, transition: transition)
-
+
if layout.inVoiceOver != wasInVoiceOver {
self.chatListDisplayNode.scrollToTop()
}
-
+
if case .chatList = self.location, let componentView = self.chatListHeaderView() {
componentView.storyComposeAction = { [weak self] offset in
guard let self else {
@@ -3100,17 +3132,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
self.openStoryCamera(fromList: true, gesturePullOffset: offset)
}
-
+
componentView.storyPeerAction = { [weak self] peer in
guard let self else {
return
}
-
+
guard let peer else {
self.chatListDisplayNode.scrollToStories(animated: true)
return
}
-
+
if peer.id == self.context.account.peerId {
if let rawStorySubscriptions = self.rawStorySubscriptions {
var openCamera = false
@@ -3119,22 +3151,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
openCamera = true
}
-
+
if openCamera {
self.openStoryCamera(fromList: true)
return
}
}
}
-
+
self.openStories(peerId: peer.id)
}
-
+
componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
guard let self else {
return
}
-
+
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id),
TelegramEngine.EngineData.Item.NotificationSettings.Global(),
@@ -3144,13 +3176,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
if peer.isService {
return
}
-
+
var items: [ContextMenuItem] = []
-
+
if peer.id == self.context.account.peerId {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextAddStory, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
@@ -3159,11 +3191,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.openStoryCamera(fromList: true)
})
})))
-
+
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextSavedStories, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
@@ -3171,11 +3203,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
})
})))
-
+
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextArchivedStories, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
@@ -3183,7 +3215,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .archive))
})
})))
@@ -3196,12 +3228,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
self.openStoryCamera(fromList: true)
})
})))
}
-
+
let openTitle: String
let openIcon: String
switch channel.info {
@@ -3219,7 +3251,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
@@ -3230,12 +3262,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let navigationController = self.navigationController as? NavigationController else {
return
}
-
+
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
})
})
})))
-
+
let hideText: String
if self.location == .chatList(groupId: .archive) {
hideText = self.presentationData.strings.StoryFeed_ContextUnarchive
@@ -3247,7 +3279,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
-
+
guard let self else {
return
}
@@ -3259,7 +3291,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true)
undoValue = false
}
-
+
if self.location == .chatList(groupId: .archive) {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .archivedChat(peerId: peer.id.toInt64(), title: "", text: self.presentationData.strings.StoryFeed_TooltipUnarchive(peer.compactDisplayTitle).string, undo: true), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action {
@@ -3288,11 +3320,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
-
+
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
})
})))
-
+
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextOpenProfile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
@@ -3300,7 +3332,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
@@ -3315,18 +3347,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})
})))
-
+
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
f(.default)
-
+
guard let self else {
return
}
let _ = self.context.engine.peers.togglePeerStoriesMuted(peerId: peer.id).startStandalone()
-
+
let iconColor = UIColor.white
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if isMuted {
@@ -3359,12 +3391,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
), in: .current)
}
})))
-
+
items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryFeed_ViewAnonymously, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: self.context.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
-
+
guard let self else {
return
}
@@ -3383,7 +3415,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
})))
-
+
let hideText: String
if self.location == .chatList(groupId: .archive) {
hideText = self.presentationData.strings.StoryFeed_ContextUnarchive
@@ -3395,7 +3427,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
-
+
guard let self else {
return
}
@@ -3407,7 +3439,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true)
undoValue = false
}
-
+
if self.location == .chatList(groupId: .archive) {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .archivedChat(peerId: peer.id.toInt64(), title: "", text: self.presentationData.strings.StoryFeed_TooltipUnarchive(peer.compactDisplayTitle).string, undo: true), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action {
@@ -3429,14 +3461,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})))
}
-
+
let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: nil, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}
}
}
-
+
public func transitionViewForOwnStoryItem() -> UIView? {
if let componentView = self.chatListHeaderView() {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
@@ -3445,30 +3477,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return nil
}
-
+
private(set) var storyUploadProgress: [PeerId: Float] = [:]
private func updateStoryUploadProgress(_ progress: [PeerId: Float]) {
self.storyUploadProgress = progress.mapValues {
max(0.027, min(0.99, $0))
}
-
+
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarView.updateStoryUploadProgress(storyUploadProgress: self.storyUploadProgress)
}
}
-
+
public func scrollToStories(peerId: EnginePeer.Id? = nil) {
self.chatListDisplayNode.scrollToStories(animated: false)
-
+
if let peerId, let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
storyPeerListView.ensureItemVisible(peerId: peerId)
}
}
-
+
public func scrollToStoriesAnimated() {
self.chatListDisplayNode.scrollToStories(animated: true)
}
-
+
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var tabContainerOffset: CGFloat = 0.0
if !self.displayNavigationBar {
@@ -3477,24 +3509,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let navigationBarHeight: CGFloat = 0.0
-
+
self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: navigationBarHeight, storiesInset: 0.0, transition: transition)
}
-
+
override public func navigationStackConfigurationUpdated(next: [ViewController]) {
super.navigationStackConfigurationUpdated(next: next)
}
-
+
public func activateEdit() {
self.editPressed()
}
-
+
public func openEmojiStatusSetup() {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarView.openEmojiStatusSetup()
}
}
-
+
@objc func editPressed() {
if self.secondaryContext == nil {
if case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location {
@@ -3515,10 +3547,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
(self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.master, transition: .animated(duration: 0.5, curve: .spring))
}
}
-
+
//TODO:update search enabled
//self.searchContentNode?.setIsEnabled(false, animated: true)
-
+
self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false
self.chatListDisplayNode.effectiveContainerNode.updateState { state in
var state = state
@@ -3531,15 +3563,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
-
+
@objc fileprivate func donePressed() {
let skipLayoutUpdate = self.reorderingDonePressed()
-
+
(self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring))
-
+
//TODO:update search enabled
//self.searchContentNode?.setIsEnabled(true, animated: true)
-
+
self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false
self.chatListDisplayNode.effectiveContainerNode.updateState { state in
var state = state
@@ -3550,14 +3582,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return state
}
self.chatListDisplayNode.isEditing = false
-
+
if !skipLayoutUpdate {
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
-
+
private var skipTabContainerUpdate = false
fileprivate func reorderingDonePressed() -> Bool {
guard let defaultFilters = self.tabContainerData else {
@@ -3573,7 +3605,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
let _ = defaultFilterIds
-
+
var reorderedFilterIdsValue: [Int32]?
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let headerPanelsView = navigationBarView.headerPanels as? HeaderPanelContainerComponent.View, let tabsView = headerPanelsView.tabs as? HorizontalTabsComponent.View, let reorderedItemIds = tabsView.reorderedItemIds {
reorderedFilterIdsValue = reorderedItemIds.compactMap { item -> Int32? in
@@ -3586,7 +3618,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return value
}
}
-
+
if let reorderedFilterIdsValue, let tabContainerData = self.tabContainerData {
var entries: [ChatListFilterTabEntry] = []
for id in reorderedFilterIdsValue {
@@ -3602,7 +3634,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
self.tabContainerData?.0 = entries
}
-
+
let completion = { [weak self] in
guard let strongSelf = self else {
return
@@ -3611,10 +3643,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.chatListDisplayNode.isReorderingFilters = false
strongSelf.isReorderingTabsValue.set(false)
(strongSelf.parent as? TabBarController)?.updateIsTabBarEnabled(true, transition: .animated(duration: 0.2, curve: .easeInOut))
-
+
//TODO:update search enabled
//strongSelf.searchContentNode?.setIsEnabled(true, animated: true)
-
+
if let layout = strongSelf.validLayout {
strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
@@ -3649,13 +3681,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return true
}
-
+
public func resetForumStackIfOpen() {
if self.secondaryContext != nil {
self.setInlineChatList(location: nil, animated: false)
}
}
-
+
public func setInlineChatList(location: ChatListControllerLocation?, animated: Bool = true) {
if let location {
let inlineNode = self.chatListDisplayNode.makeInlineChatList(location: location)
@@ -3668,7 +3700,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
isReorderingTabs: .single(false),
storyPostingAvailable: .single(false)
)
-
+
self.pendingSecondaryContext = pendingSecondaryContext
let _ = (pendingSecondaryContext.ready.get()
|> filter { $0 }
@@ -3677,11 +3709,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self, let pendingSecondaryContext = pendingSecondaryContext, self.pendingSecondaryContext === pendingSecondaryContext else {
return
}
-
+
if self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing {
self.donePressed()
}
-
+
self.secondaryContext = pendingSecondaryContext
self.setToolbar(pendingSecondaryContext.toolbar, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
self.chatListDisplayNode.setInlineChatList(inlineStackContainerNode: inlineNode)
@@ -3691,29 +3723,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing {
self.donePressed()
}
-
+
self.secondaryContext = nil
self.setToolbar(self.primaryContext?.toolbar, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
self.chatListDisplayNode.setInlineChatList(inlineStackContainerNode: nil)
self.updateNavigationMetadata()
}
}
-
+
private func navigationBackPressed() {
self.dismiss()
}
-
+
public static func openMoreMenu(context: AccountContext, peerId: EnginePeer.Id, sourceController: ViewController, isViewingAsTopics: Bool, sourceView: UIView, gesture: ContextGesture?) {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard case let .channel(channel) = peer else {
return
}
-
+
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
-
+
var items: [ContextMenuItem] = []
-
+
items.append(.action(ContextMenuActionItem(text: strings.Chat_ContextViewAsTopics, icon: { theme in
if !isViewingAsTopics {
return UIImage()
@@ -3721,11 +3753,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak sourceController] _, a in
a(.default)
-
+
guard let sourceController = sourceController, let navigationController = sourceController.navigationController as? NavigationController else {
return
}
-
+
if let targetController = navigationController.viewControllers.first(where: { controller in
var checkController = controller
if let tabBarController = checkController as? TabBarController {
@@ -3747,7 +3779,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let chatController = context.sharedContext.makeChatListController(context: context, location: .forum(peerId: peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false)
navigationController.replaceController(sourceController, with: chatController, animated: false)
}
-
+
let _ = context.engine.peers.updateForumViewAsMessages(peerId: peerId, value: false).startStandalone()
})))
items.append(.action(ContextMenuActionItem(text: strings.Chat_ContextViewAsMessages, icon: { theme in
@@ -3761,9 +3793,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let sourceController = sourceController, let navigationController = sourceController.navigationController as? NavigationController else {
return
}
-
+
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
-
+
if let sourceController = sourceController as? ChatListControllerImpl, case .forum(peerId) = sourceController.location {
navigationController.replaceController(sourceController, with: chatController, animated: false)
} else {
@@ -3780,16 +3812,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
navigationController.pushViewController(chatController, animated: false)
}
-
+
let _ = context.engine.peers.updateForumViewAsMessages(peerId: peerId, value: true).startStandalone()
})))
items.append(.separator)
-
+
items.append(.action(ContextMenuActionItem(text: strings.GroupInfo_Title, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor)
}, action: { [weak sourceController] _, f in
f(.default)
-
+
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
@@ -3800,13 +3832,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
(sourceController.navigationController as? NavigationController)?.pushViewController(controller)
})
})))
-
+
if channel.hasPermission(.inviteMembers) {
items.append(.action(ContextMenuActionItem(text: strings.GroupInfo_AddParticipant, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { [weak sourceController] _, f in
f(.default)
-
+
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let sourceController = sourceController, let peer = peer else {
@@ -3818,7 +3850,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
var needsSeparatorForCreateTopic = true
if let sourceController = sourceController as? ChatController {
items.append(.separator)
@@ -3826,7 +3858,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor)
}, action: { [weak sourceController] action in
action.dismissWithResult(.default)
-
+
sourceController?.beginMessageSearch("")
})))
needsSeparatorForCreateTopic = false
@@ -3835,19 +3867,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if needsSeparatorForCreateTopic {
items.append(.separator)
}
-
+
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: { action in
action.dismissWithResult(.default)
-
+
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
controller.navigationPresentation = .modal
-
+
controller.completion = { [weak controller] title, fileId, iconColor, _ in
controller?.isInProgress = true
controller?.view.endEditing(true)
-
+
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId)
|> deliverOnMainQueue).startStandalone(next: { topicId in
if let navigationController = (sourceController.navigationController as? NavigationController) {
@@ -3866,10 +3898,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
sourceController.presentInGlobalOverlay(contextController)
})
}
-
+
func openArchiveMoreMenu(sourceView: UIView, gesture: ContextGesture?) {
let _ = self.context.engine.privacy.updateGlobalPrivacySettings().startStandalone()
-
+
let _ = (
self.context.engine.messages.chatList(group: .archive, count: 10) |> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] archiveChatList in
@@ -3877,26 +3909,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
-
+
var items: [ContextMenuItem] = []
-
+
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Archive_ContextSettings, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
-
+
guard let self else {
return
}
self.push(self.context.sharedContext.makeArchiveSettingsController(context: self.context))
})))
-
+
if !archiveChatList.items.isEmpty {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Archive_ContextInfo, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Question"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
-
+
guard let self else {
return
}
@@ -3914,7 +3946,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
-
+
guard let self else {
return
}
@@ -3926,7 +3958,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.presentInGlobalOverlay(contextController)
})
}
-
+
private var initializedFilters = false
private func reloadFilters(firstUpdate: (() -> Void)? = nil) {
let filterItems = chatListFilterItems(context: self.context)
@@ -3940,13 +3972,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
let isPremium = peerView.peers[peerView.peerId]?.isPremium
strongSelf.isPremium = isPremium ?? false
-
+
let (_, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
-
+
for (filter, unreadCount, hasUnmutedUnread) in items {
switch filter {
case .allChats:
@@ -3959,13 +3991,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
filterItems.append(.filter(id: id, text: title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread)))
}
}
-
+
var resolvedItems = filterItems
if case .chatList(.root) = strongSelf.location {
} else {
resolvedItems = []
}
-
+
let firstItem = countAndFilterItems.1.first?.0 ?? .allChats
let firstItemEntryId: ChatListFilterTabEntryId
switch firstItem {
@@ -3974,7 +4006,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case let .filter(id, _, _, _):
firstItemEntryId = .filter(id)
}
-
+
var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.mainContainerNode.currentItemFilter
var resetCurrentEntry = false
if !resolvedItems.contains(where: { $0.id == selectedEntryId }) {
@@ -4018,7 +4050,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
availableFilters.insert(.all, at: 0)
}
strongSelf.chatListDisplayNode.mainContainerNode.updateAvailableFilters(availableFilters, limit: filtersLimit)
-
+
if isPremium == nil && items.isEmpty {
strongSelf.mainReady.set(strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.ready)
} else if !strongSelf.initializedFilters {
@@ -4033,27 +4065,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
strongSelf.initializedFilters = true
}
-
+
let animated = strongSelf.didSetupTabs
strongSelf.didSetupTabs = true
-
+
if let layout = strongSelf.validLayout {
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 {
notifiedFirstUpdate = true
firstUpdate?()
}
-
+
if resetCurrentEntry {
strongSelf.selectTab(id: selectedEntryId, switchToChatsIfNeeded: false)
}
}))
}
-
+
func selectTab(id: ChatListFilterTabEntryId, switchToChatsIfNeeded: Bool = true) {
if self.parent == nil, switchToChatsIfNeeded {
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
@@ -4067,7 +4099,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let _ = (self.context.engine.peers.currentChatListFilters()
|> deliverOnMainQueue).startStandalone(next: { [weak self] filters in
guard let strongSelf = self else {
@@ -4103,7 +4135,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
private func readAllInFilter(id: Int32) {
for filter in self.chatListDisplayNode.mainContainerNode.availableFilters {
if case let .filter(filter) = filter, case let .filter(filterId, _, _, data) = filter, filterId == id {
@@ -4113,13 +4145,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds {
markItems.append((EngineChatList.Group(additionalGroupId), filterPredicate))
}
-
+
let _ = self.context.engine.messages.markAllChatsAsReadInteractively(items: markItems).startStandalone()
break
}
}
}
-
+
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: ChatFolderTitle) {
let presentationData = self.presentationData
let progressSignal = Signal { [weak self] subscriber in
@@ -4134,7 +4166,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal<[ExportedChatFolderLink]?, NoError> = self.context.engine.peers.getExportedChatFolderLinks(id: filterId)
|> afterDisposed {
Queue.mainQueue().async {
@@ -4146,7 +4178,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
if links == nil || links?.count == 0 {
openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: false, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
self?.push(c)
@@ -4174,7 +4206,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {
let _ = (self.chatListDisplayNode.mainContainerNode.availableFiltersSignal
|> filter { filters in
@@ -4195,7 +4227,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
if self.chatListDisplayNode.inlineStackContainerNode != nil {
self.setInlineChatList(location: nil)
}
@@ -4208,7 +4240,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
public func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32) {
let presentationData = self.presentationData
let progressSignal = Signal { [weak self] subscriber in
@@ -4223,7 +4255,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal = self.context.engine.messages.peerStoriesAreReady(
id: peerId,
minId: storyId
@@ -4238,7 +4270,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
progressDisposable.dispose()
}
}
-
+
self.sharedOpenStoryProgressDisposable.set((signal |> deliverOnMainQueue).startStrict(completed: { [weak self] in
guard let self else {
return
@@ -4267,24 +4299,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)
}))
}
-
+
public func openStories(peerId: EnginePeer.Id) {
self.openStories(peerId: peerId, completion: { _ in })
}
-
+
public func openStories(peerId: EnginePeer.Id, completion: @escaping (StoryContainerScreen) -> Void = { _ in }) {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
self.shouldFixStorySubscriptionOrder = true
}
}
-
+
if peerId != self.context.account.peerId {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
let _ = storyPeerListView
-
+
var initialOrder: [EnginePeer.Id] = []
if let orderedStorySubscriptions = self.orderedStorySubscriptions {
if let accountItem = orderedStorySubscriptions.accountItem {
@@ -4296,7 +4328,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
initialOrder.append(item.peer.id)
}
}
-
+
StoryContainerScreen.openPeerStoriesCustom(
context: self.context,
peerId: peerId,
@@ -4319,12 +4351,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
sourceCornerRadius: transitionView.bounds.height * 0.5,
sourceIsAvatar: true
)
-
+
Queue.mainQueue().after(0.3, { [weak self] in
guard let self else {
return
}
-
+
self.chatListDisplayNode.mainContainerNode.currentItemNode.scroller.panGestureRecognizer.state = .cancelled
})
}
@@ -4337,11 +4369,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return nil
}
-
+
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
self.scrollToStories()
-
+
if let componentView = self.chatListHeaderView() {
if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
@@ -4356,7 +4388,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
return nil
},
setFocusedItem: { [weak self] focusedItem in
@@ -4378,13 +4410,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
},
completion: completion
)
-
+
return
}
}
}
}
-
+
let storyContent = StoryContentContextImpl(context: self.context, isHidden: self.location == .chatList(groupId: .archive), focusedPeerId: peerId, singlePeer: false, fixedOrder: self.fixedStorySubscriptionOrder)
let _ = (storyContent.state
|> take(1)
@@ -4392,12 +4424,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
if peerId == self.context.account.peerId, storyContentState.slice == nil {
self.openStoryCamera(fromList: true)
return
}
-
+
var transitionIn: StoryContainerScreen.TransitionIn?
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
@@ -4413,7 +4445,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
@@ -4422,7 +4454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return nil
}
-
+
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView() {
@@ -4439,7 +4471,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
return nil
}
)
@@ -4449,29 +4481,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(storyContainerScreen)
})
}
-
+
func askForFilterRemoval(id: Int32) {
let apply: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
-
+
let commit: () -> Void = {
guard let strongSelf = self else {
return
}
-
+
if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id {
if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing {
strongSelf.donePressed()
}
}
-
+
let _ = (strongSelf.context.engine.peers.updateChatListFiltersInteractively { filters in
return filters.filter({ $0.id != id })
}).startStandalone()
}
-
+
if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id {
strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: .all, completion: {
commit()
@@ -4480,7 +4512,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
commit()
}
}
-
+
let _ = (self.context.engine.peers.currentChatListFilters()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] filters in
@@ -4490,7 +4522,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let filter = filters.first(where: { $0.id == id }), case let .filter(_, title, _, data) = filter else {
return
}
-
+
if data.isShared {
let _ = (combineLatest(
self.context.engine.data.get(
@@ -4504,28 +4536,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
let presentationData = self.presentationData
-
+
let peers = peerData.0
-
+
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
-
+
var hasLinks = false
if let links, !links.isEmpty {
hasLinks = true
}
-
+
let confirmDeleteFolder: () -> Void = { [weak self] in
guard let self else {
return
}
-
+
let filteredPeers = peers.compactMap { $0 }.filter { peer in
if case .channel = peer {
return true
@@ -4559,7 +4591,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(previewScreen)
}
}
-
+
if hasLinks {
self.present(textAlertController(context: self.context, title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
@@ -4584,32 +4616,32 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
public private(set) var isSearchActive: Bool = false
-
+
public func activateSearch(filter: ChatListSearchFilter, query: String? = nil) {
self.activateSearchInternal(isFromTabBar: false, filter: filter, query: query)
}
-
+
public func activateSearchInternal(isFromTabBar: Bool, filter: ChatListSearchFilter, query: String? = nil) {
var searchContentNode: NavigationBarSearchContentNode?
if !isFromTabBar, let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
searchContentNode = navigationBarView.searchContentNode
}
-
+
self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode)
}
-
+
public func activateSearch(query: String? = nil) {
var isForum = false
if case .forum = self.location {
isForum = true
}
-
+
let filter: ChatListSearchFilter = isForum ? .topics : .chats
self.activateSearch(filter: filter, query: query)
}
-
+
private var previousSearchToggleTimestamp: Double?
func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode?) {
Task { @MainActor [weak self] in
@@ -4622,37 +4654,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
self.previousSearchToggleTimestamp = currentTimestamp
-
+
if let storyTooltip = self.storyTooltip {
storyTooltip.dismiss()
}
-
+
var filter = filter
if case .forum = self.chatListDisplayNode.effectiveContainerNode.location {
filter = .topics
}
-
- if self.chatListDisplayNode.searchDisplayController == nil {
+
+ if self.chatListDisplayNode.searchDisplayController == nil {
let (_, _) = await combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)).get()
do {
let displaySearchFilters = true
-
+
if let filterContainerNodeAndActivate = await self.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode?.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: self.hasDownloads, initialFilter: filter, navigationController: self.navigationController as? NavigationController, searchBarIsExternal: searchContentNode == nil) {
let activate = filterContainerNodeAndActivate
-
+
activate(filter != .downloads)
-
+
if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
searchContentNode.search(filter: filter, query: query)
}
}
-
+
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.setDisplayNavigationBar(false, transition: transition)
if searchContentNode == nil {
self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: true), transition: transition)
-
+
if let searchBarNode = self.currentTabBarSearchNode?() as? SearchBarNode {
self.chatListDisplayNode.searchDisplayController?.setSearchBar(searchBarNode)
searchBarNode.activate()
@@ -4678,7 +4710,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
public func deactivateSearch(animated: Bool) {
guard !self.displayNavigationBar else {
return
@@ -4688,14 +4720,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
self.previousSearchToggleTimestamp = currentTimestamp
-
+
var completion: (() -> Void)?
-
+
var searchContentNode: NavigationBarSearchContentNode?
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
searchContentNode = navigationBarView.searchContentNode
}
-
+
if let searchContentNode {
let previousFrame = searchContentNode.placeholderNode.frame
if case .chatList(.root) = self.location {
@@ -4706,19 +4738,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: nil, animated: animated)
}
-
+
self.chatListDisplayNode.tempAllowAvatarExpansion = true
self.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
self.chatListDisplayNode.tempAllowAvatarExpansion = false
-
+
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
self.setDisplayNavigationBar(true, transition: transition)
-
+
completion?()
-
+
self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: transition)
(self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: transition)
-
+
self.isSearchActive = false
if let navigationController = self.navigationController as? NavigationController {
for controller in navigationController.globalOverlayControllers {
@@ -4729,18 +4761,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
public func activateCompose() {
self.composePressed()
}
-
+
@objc fileprivate func composePressed() {
guard !self.context.isFrozen else {
let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)
self.push(controller)
return
}
-
+
guard let navigationController = self.navigationController as? NavigationController else {
return
}
@@ -4750,16 +4782,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
hasComposeController = true
}
}
-
+
if !hasComposeController {
let controller = self.context.sharedContext.makeComposeController(context: self.context)
navigationController.pushViewController(controller)
}
}
-
+
public override var keyShortcuts: [KeyShortcut] {
let strings = self.presentationData.strings
-
+
let toggleSearch: () -> Void = { [weak self] in
if let strongSelf = self {
if strongSelf.displayNavigationBar {
@@ -4769,7 +4801,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let inputShortcuts: [KeyShortcut] = [
KeyShortcut(title: strings.KeyCommand_JumpToPreviousChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate], action: { [weak self] in
if let strongSelf = self {
@@ -4804,7 +4836,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
KeyShortcut(title: strings.KeyCommand_Find, input: "\t", modifiers: [], action: toggleSearch),
KeyShortcut(input: UIKeyCommand.inputEscape, modifiers: [], action: toggleSearch)
]
-
+
let openTab: (Int) -> Void = { [weak self] index in
if let strongSelf = self {
let filters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters
@@ -4819,7 +4851,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let openChat: (Int) -> Void = { [weak self] index in
if let strongSelf = self {
if index == 0 {
@@ -4829,7 +4861,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let folderShortcuts: [KeyShortcut] = (0 ... 9).map { index in
return KeyShortcut(input: "\(index)", modifiers: [.command], action: {
if index == 0 {
@@ -4839,16 +4871,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
let chatShortcuts: [KeyShortcut] = (0 ... 9).map { index in
return KeyShortcut(input: "\(index)", modifiers: [.command, .alternate], action: {
openChat(index)
})
}
-
+
return inputShortcuts + folderShortcuts + chatShortcuts
}
-
+
override public func toolbarActionSelected(action: ToolbarActionOption) {
let peerIds = self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.selectedPeerIds
let threadIds = self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.selectedThreadIds
@@ -4896,11 +4928,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteThreadsConfirmation(Int32(threadIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
guard let strongSelf = self else {
return
}
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first))
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
@@ -4909,9 +4941,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count))
-
+
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
@@ -4930,7 +4962,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal = strongSelf.context.engine.peers.removeForumChannelThreads(id: peerId, threadIds: Array(threadIds))
|> afterDisposed {
Queue.mainQueue().async {
@@ -4939,7 +4971,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let _ = (signal
|> deliverOnMainQueue).start()
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for threadId in threadIds {
@@ -4947,7 +4979,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadIds.first))
@@ -4963,10 +4995,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return false
}), in: .current)
-
+
strongSelf.donePressed()
}))
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
@@ -4984,7 +5016,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
var havePrivateChats = false
var haveNonPrivateChats = false
for peer in peers {
@@ -4997,17 +5029,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
if havePrivateChats {
items.append(ActionSheetButtonItem(title: haveNonPrivateChats ? self.presentationData.strings.ChatList_DeleteForAllWhenPossible : self.presentationData.strings.ChatList_DeleteForAll, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
guard let strongSelf = self else {
return
}
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
@@ -5015,9 +5047,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
-
+
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
@@ -5036,7 +5068,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds), deleteGloballyIfPossible: true)
|> afterDisposed {
Queue.mainQueue().async {
@@ -5045,7 +5077,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let _ = (signal
|> deliverOnMainQueue).start()
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
@@ -5053,7 +5085,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
@@ -5069,17 +5101,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return false
}), in: .current)
-
+
strongSelf.donePressed()
}))
}
items.append(ActionSheetButtonItem(title: havePrivateChats ? self.presentationData.strings.ChatList_DeleteForMe : self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
guard let strongSelf = self else {
return
}
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
@@ -5087,9 +5119,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
-
+
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
@@ -5108,7 +5140,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds))
|> afterDisposed {
Queue.mainQueue().async {
@@ -5117,7 +5149,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let _ = (signal
|> deliverOnMainQueue).start()
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
@@ -5125,7 +5157,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return state
})
-
+
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
@@ -5141,10 +5173,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return false
}), in: .current)
-
+
strongSelf.donePressed()
}))
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
@@ -5191,14 +5223,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
-
+
let signal: Signal = self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peerId, hash: nil)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
-
+
var didJoin = false
self.joinForumDisposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
@@ -5220,9 +5252,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self, let peer = peer else {
return
}
-
+
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
-
+
let text: String
switch error {
case .inviteRequestSent:
@@ -5284,7 +5316,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
func toggleArchivedFolderHiddenByDefault() {
var updatedValue = false
let _ = (updateChatArchiveSettings(engine: self.context.engine, { settings in
@@ -5311,7 +5343,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return true
})
-
+
if updatedValue {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let strongSelf = self else {
@@ -5323,7 +5355,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
settings.isHiddenByDefault = false
return settings
}).startStandalone()
-
+
return true
}
return false
@@ -5334,7 +5366,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
func hidePsa(_ id: PeerId) {
self.chatListDisplayNode.mainContainerNode.updateState { state in
var state = state
@@ -5342,10 +5374,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
state.peerIdWithRevealedOptions = nil
return state
}
-
+
let _ = hideAccountPromoInfoChat(account: self.context.account, peerId: id).startStandalone()
}
-
+
func deletePeerChat(peerId: PeerId, joined: Bool) {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.RenderedPeer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
@@ -5353,7 +5385,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
strongSelf.view.window?.endEditing(true)
-
+
var canRemoveGlobally = false
let limitsConfiguration = strongSelf.context.currentLimitsConfiguration.with { $0 }
if peer.peerId.namespace == Namespaces.Peer.CloudUser && peer.peerId != strongSelf.context.account.peerId {
@@ -5363,7 +5395,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else if peer.peerId.namespace == Namespaces.Peer.SecretChat {
canRemoveGlobally = true
}
-
+
if case let .user(user) = chatPeer, user.botInfo == nil, canRemoveGlobally {
strongSelf.maybeAskForPeerChatRemoval(peer: peer, joined: joined, completion: { _ in }, removed: {})
} else {
@@ -5372,7 +5404,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var canClear = true
var canStop = false
var canRemoveGlobally = false
-
+
var deleteTitle = strongSelf.presentationData.strings.Common_Delete
if case let .channel(channel) = chatPeer {
if channel.isMonoForum {
@@ -5403,7 +5435,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
canClear = true
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
}
-
+
let limitsConfiguration = strongSelf.context.currentLimitsConfiguration.with { $0 }
if case .user = chatPeer, chatPeer.id != strongSelf.context.account.peerId {
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
@@ -5412,7 +5444,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else if case .secretChat = chatPeer {
canRemoveGlobally = true
}
-
+
var isGroupOrChannel = false
switch mainPeer {
case .legacyGroup, .channel:
@@ -5420,18 +5452,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
default:
break
}
-
+
if canRemoveGlobally && isGroupOrChannel {
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .deleteAndLeave, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
-
+
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
let proceed = {
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: false, completion: {
})
}
-
+
let shouldCheckFutureCreator: Bool
if case let .channel(channel) = peer.peer, channel.flags.contains(.isCreator) {
shouldCheckFutureCreator = true
@@ -5440,7 +5472,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
shouldCheckFutureCreator = false
}
-
+
if let self, shouldCheckFutureCreator {
let _ = (self.context.engine.peers.getFutureCreatorAfterLeave(peerId: peer.peerId)
|> deliverOnMainQueue).start(next: { [weak self] nextCreator in
@@ -5461,27 +5493,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
proceed()
}
}))
-
+
let deleteForAllText: String
if case let .channel(channel) = mainPeer, case .broadcast = channel.info {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribers
} else {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembers
}
-
+
items.append(ActionSheetButtonItem(title: deleteForAllText, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
-
+
let deleteForAllConfirmation: String
if case let .channel(channel) = mainPeer, case .broadcast = channel.info {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteChannelConfirmation
} else {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteGroupConfirmation
}
-
+
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
@@ -5493,11 +5525,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
} else {
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
-
+
if canStop {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
if let strongSelf = self {
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in
}, removed: {
@@ -5509,7 +5541,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}))
}
-
+
if canClear {
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
guard let strongSelf = self else {
@@ -5526,7 +5558,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return true
})
-
+
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: strongSelf.presentationData.strings.Undo_ChatCleared), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
@@ -5554,23 +5586,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}), in: .current)
}
-
+
items.append(ActionSheetButtonItem(title: canStop ? strongSelf.presentationData.strings.DialogList_DeleteBotClearHistory : strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
guard let strongSelf = self else {
return
}
-
+
if case .secretChat = chatPeer {
beginClear(.forEveryone)
} else {
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
-
+
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory(canClearCache: false), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
-
+
if joined || mainPeer.isDeleted {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
beginClear(.forEveryone)
@@ -5586,7 +5618,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
actionSheet?.dismissAnimated()
}))
}
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
@@ -5608,7 +5640,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}))
}
-
+
if case .secretChat = chatPeer {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
@@ -5624,7 +5656,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
var isGroupOrChannel = false
switch mainPeer {
case .legacyGroup, .channel:
@@ -5632,39 +5664,39 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
default:
break
}
-
+
if canRemoveGlobally && isGroupOrChannel {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
-
+
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .deleteAndLeave, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
-
+
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: false, completion: {
})
}))
-
+
let deleteForAllText: String
if case let .channel(channel) = mainPeer, case .broadcast = channel.info {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribers
} else {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembers
}
-
+
items.append(ActionSheetButtonItem(title: deleteForAllText, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
-
+
let deleteForAllConfirmation: String
if case let .channel(channel) = mainPeer, case .broadcast = channel.info {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribersConfirmationText
} else {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText
}
-
+
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
@@ -5674,7 +5706,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
], parseMarkdown: true), in: .window(.root))
}))
-
+
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
@@ -5690,7 +5722,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
}
}
-
+
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
@@ -5702,17 +5734,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
}
-
+
func deletePeerThread(peerId: EnginePeer.Id, threadId: Int64) {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
-
+
items.append(ActionSheetTextItem(title: self.presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.commitDeletePeerThread(peerId: peerId, threadId: threadId, completion: {})
}))
-
+
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
@@ -5722,7 +5754,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
])
self.present(actionSheet, in: .window(.root))
}
-
+
func selectPeerThread(peerId: EnginePeer.Id, threadId: Int64) {
self.chatListDisplayNode.effectiveContainerNode.updateState({ state in
var state = state
@@ -5731,7 +5763,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
self.chatListDisplayNode.effectiveContainerNode.didBeginSelectingChats?()
}
-
+
private func commitDeletePeerThread(peerId: EnginePeer.Id, threadId: Int64, completion: @escaping () -> Void) {
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
@@ -5739,23 +5771,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
return true
})
-
+
self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId))
self.chatListDisplayNode.effectiveContainerNode.updateState({ state in
var state = state
state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId))
return state
})
-
+
let statusText = self.presentationData.strings.Undo_DeletedTopic
-
+
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let self else {
return false
}
if value == .commit {
self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId))
-
+
let _ = self.context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).startStandalone(completed: { [weak self] in
guard let self else {
return
@@ -5767,13 +5799,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil)
})
-
+
self.chatListDisplayNode.effectiveContainerNode.updateState({ state in
var state = state
state.selectedThreadIds.remove(threadId)
return state
})
-
+
completion()
return true
} else if value == .undo {
@@ -5789,15 +5821,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}), in: .current)
}
-
+
private func setPeerThreadStopped(peerId: EnginePeer.Id, threadId: Int64, isStopped: Bool) {
self.actionDisposables.add(self.context.engine.peers.setForumChannelTopicClosed(id: peerId, threadId: threadId, isClosed: isStopped).startStrict())
}
-
+
private func setPeerThreadPinned(peerId: EnginePeer.Id, threadId: Int64, isPinned: Bool) {
self.actionDisposables.add(self.context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId).startStrict())
}
-
+
private func setPeerThreadHidden(peerId: EnginePeer.Id, threadId: Int64, isHidden: Bool) {
self.actionDisposables.add((self.context.engine.peers.setForumChannelTopicHidden(id: peerId, threadId: threadId, isHidden: isHidden)
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
@@ -5807,7 +5839,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
state.hiddenItemShouldBeTemporaryRevealed = false
return state
}
-
+
if isHidden {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_GeneralHidden, text: strongSelf.presentationData.strings.ChatList_GeneralHiddenInfo, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let strongSelf = self else {
@@ -5826,7 +5858,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}))
}
-
+
public func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool = false, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) {
guard let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else {
completion(false)
@@ -5845,7 +5877,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case .secretChat = chatPeer {
canRemoveGlobally = true
}
-
+
if deleteGloballyIfPossible && self.canDeletePeerGloballyAsCreator(mainPeer) {
self.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
removed()
@@ -5853,7 +5885,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
completion(true)
return
}
-
+
if canRemoveGlobally {
var actions: [AlertScreen.Action] = []
if joined || mainPeer.isDeleted {
@@ -5870,7 +5902,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
completion(true)
}))
-
+
actions.append(.init(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, type: .destructive, action: { [weak self] in
guard let strongSelf = self else {
return
@@ -5889,7 +5921,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}))
}
actions.append(.init(title: self.presentationData.strings.Common_Cancel))
-
+
let title: String = self.presentationData.strings.ChatList_DeleteChat
var text: String
if mainPeer.id == self.context.account.peerId {
@@ -5903,7 +5935,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
text = self.presentationData.strings.ChatList_DeleteChatConfirmation("**\(chatPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))**").string
}
-
+
let alertScreen = AlertScreen(
context: self.context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
@@ -5957,7 +5989,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
removed()
})
}
-
+
let shouldCheckFutureCreator: Bool
if case let .channel(channel) = peer.peer, channel.flags.contains(.isCreator) {
shouldCheckFutureCreator = true
@@ -5966,7 +5998,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
shouldCheckFutureCreator = false
}
-
+
if shouldCheckFutureCreator {
let _ = (self.context.engine.peers.getFutureCreatorAfterLeave(peerId: peer.peerId)
|> deliverOnMainQueue).start(next: { [weak self] nextCreator in
@@ -5990,7 +6022,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
}
-
+
private func canDeletePeerGloballyAsCreator(_ peer: EnginePeer) -> Bool {
if case let .channel(channel) = peer {
return !channel.isMonoForum && channel.flags.contains(.isCreator) && channel.addressName == nil
@@ -6000,19 +6032,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}
}
-
+
func archiveChats(peerIds: [PeerId]) {
guard !peerIds.isEmpty else {
return
}
let engine = self.context.engine
-
+
let hasArchived = engine.messages.chatList(group: .archive, count: 10)
|> take(1)
|> map { list -> Bool in
return !list.items.isEmpty
}
-
+
self.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds[0], threadId: nil))
let _ = (combineLatest(
ApplicationSpecificNotice.incrementArchiveChatTips(accountManager: self.context.sharedContext.accountManager, count: 1),
@@ -6025,11 +6057,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.setCurrentRemovingItemId(nil)
-
+
for peerId in peerIds {
deleteSendMessageIntents(peerId: peerId)
}
-
+
let action: (UndoOverlayAction) -> Bool = { value in
guard let strongSelf = self else {
return false
@@ -6048,14 +6080,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}
}
-
+
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
}
return true
})
-
+
var title = peerIds.count == 1 ? strongSelf.presentationData.strings.ChatList_UndoArchiveTitle : strongSelf.presentationData.strings.ChatList_UndoArchiveMultipleTitle
let text: String
let undo: Bool
@@ -6069,22 +6101,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let controller = UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .archivedChat(peerId: peerIds[0].toInt64(), title: title, text: text, undo: undo), elevatedLayout: false, animateInAsReplacement: true, action: action)
strongSelf.present(controller, in: .current)
-
+
strongSelf.chatListDisplayNode.playArchiveAnimation()
})
})
}
-
+
private func schedulePeerChatRemoval(peer: EngineRenderedPeer, type: InteractiveMessagesDeletionType, deleteGloballyIfPossible: Bool, completion: @escaping () -> Void) {
guard let chatPeer = peer.peers[peer.peerId] else {
return
}
-
+
var deleteGloballyIfPossible = deleteGloballyIfPossible
if case .forEveryone = type {
deleteGloballyIfPossible = true
}
-
+
let peerId = peer.peerId
self.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
self.chatListDisplayNode.effectiveContainerNode.updateState({ state in
@@ -6125,14 +6157,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
statusText = self.presentationData.strings.Undo_ChatDeleted
}
}
-
+
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
}
return true
})
-
+
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let strongSelf = self else {
return false
@@ -6152,16 +6184,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return state
})
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(nil)
-
+
deleteSendMessageIntents(peerId: peerId)
})
-
+
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState({ state in
var state = state
state.selectedPeerIds.remove(peerId)
return state
})
-
+
completion()
return true
} else if value == .undo {
@@ -6177,7 +6209,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}), in: .current)
}
-
+
override public func setToolbar(_ toolbar: Toolbar?, transition: ContainedViewLayoutTransition) {
if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location {
super.setToolbar(toolbar, transition: transition)
@@ -6186,7 +6218,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.requestLayout(transition: transition)
}
}
-
+
public var lockViewFrame: CGRect? {
if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), let lockViewFrame = storyPeerListView.lockViewFrame() {
return storyPeerListView.convert(lockViewFrame, to: self.view)
@@ -6194,7 +6226,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
}
-
+
private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
@@ -6204,11 +6236,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
navigationController.pushViewController(controller)
}
}
-
+
override public func tabBarDisabledAction() {
self.donePressed()
}
-
+
override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) {
let _ = (combineLatest(queue: .mainQueue(),
self.context.engine.peers.currentChatListFilters(),
@@ -6224,13 +6256,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
-
+
let (accountPeer, limits, _) = result
let isPremium = accountPeer?.isPremium ?? false
-
+
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().startStandalone()
let (_, filterItems) = filterItemsAndTotalCount
-
+
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presetList.isEmpty ? strongSelf.presentationData.strings.ChatList_AddFolder : strongSelf.presentationData.strings.ChatList_EditFolders, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: presetList.isEmpty ? "Chat/Context Menu/Add" : "Chat/Context Menu/ItemList"), color: theme.contextMenu.primaryColor)
@@ -6242,7 +6274,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.openFilterSettings()
})
})))
-
+
if strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter != nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_FolderAllChats, icon: { theme in
return nil
@@ -6254,7 +6286,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.selectTab(id: .all)
})))
}
-
+
if !presetList.isEmpty {
if presetList.count > 1 {
items.append(.separator)
@@ -6267,7 +6299,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if !isPremium && filterCount >= limits.maxFoldersCount {
isDisabled = true
}
-
+
for item in filterItems {
if item.0.id == id && item.1 != 0 {
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
@@ -6321,11 +6353,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.selectTab(id: .filter(id))
}
})))
-
+
filterCount += 1
}
}
-
+
let controller = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
@@ -6338,7 +6370,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
override public func tabBarDeactivateSearch() {
self.deactivateSearch(animated: true)
}
-
+
private var playedSignUpCompletedAnimation = false
public func playSignUpCompletedAnimation() {
guard !self.playedSignUpCompletedAnimation else {
@@ -6349,11 +6381,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
}
-
+
func openBirthdaySetup() {
let context = self.context
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupBirthday.id).startStandalone()
-
+
let settingsPromise: Promise
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() {
settingsPromise = current
@@ -6361,7 +6393,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
settingsPromise = Promise()
settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init)))
}
-
+
let controller = context.sharedContext.makeBirthdayPickerScreen(
context: context,
settings: settingsPromise,
@@ -6377,9 +6409,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
-
+
let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone()
-
+
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in
return true
@@ -6388,7 +6420,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)
self.push(controller)
}
-
+
func openStarsTopup(amount: Int64?) {
guard let starsContext = self.context.starsContext else {
return
@@ -6396,14 +6428,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, targetPeerId: nil, customTheme: nil, completion: { _ in })
self.push(controller)
}
-
+
func openAdInfo(node: ASDisplayNode, adPeer: AdPeer) {
let controller = self
let referenceView = node.view
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
-
+
var actions: [ContextMenuItem] = []
if adPeer.sponsorInfo != nil || adPeer.additionalInfo != nil {
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in
@@ -6411,22 +6443,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}, iconSource: nil, action: { [weak self] c, _ in
let _ = self
var subItems: [ContextMenuItem] = []
-
+
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, iconPosition: .left, action: { c, _ in
c?.popItems()
})))
-
+
subItems.append(.separator)
-
+
if let sponsorInfo = adPeer.sponsorInfo {
subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return nil
}, iconSource: nil, action: { [weak self] c, _ in
c?.dismiss(completion: {
UIPasteboard.general.string = sponsorInfo
-
+
if let self {
self.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied), elevatedLayout: false, action: { _ in
return true
@@ -6441,7 +6473,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}, iconSource: nil, action: { [weak self] c, _ in
c?.dismiss(completion: {
UIPasteboard.general.string = additionalInfo
-
+
if let self {
self.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied), elevatedLayout: false, action: { _ in
return true
@@ -6450,11 +6482,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
}
-
+
c?.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
})))
}
-
+
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AboutAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] _, f in
@@ -6463,16 +6495,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.push(AdsInfoScreen(context: self.context, mode: .search))
}
})))
-
+
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] _, f in
f(.default)
-
+
guard let navigationController = self?.navigationController as? NavigationController else {
return
}
-
+
let _ = (context.engine.messages.reportAdMessage(opaqueId: adPeer.opaqueId, option: nil)
|> deliverOnMainQueue).start(next: { [weak navigationController] result in
if case let .options(title, options) = result {
@@ -6491,9 +6523,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
})))
-
+
actions.append(.separator)
-
+
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] c, _ in
@@ -6508,9 +6540,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.ReportAd_Hidden, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in
return true
}), in: .current)
-
+
let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start()
-
+
if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
searchContentNode.removeAds()
}
@@ -6527,11 +6559,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
})))
-
+
let contextController = makeContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
-
+
private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator?
var hasStoryCameraTransition: Bool {
return self.storyCameraTransitionInCoordinator != nil
@@ -6540,7 +6572,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else {
return
}
-
+
let coordinator: StoryCameraTransitionInCoordinator?
if let current = self.storyCameraTransitionInCoordinator {
coordinator = current
@@ -6559,7 +6591,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case .botPreview:
peerId = nil
}
-
+
if let peerId, let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
@@ -6574,14 +6606,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
coordinator?.updateTransitionProgress(transitionFraction)
}
-
+
func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) {
if let coordinator = self.storyCameraTransitionInCoordinator {
coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity)
self.storyCameraTransitionInCoordinator = nil
}
}
-
+
var isStoryPostingAvailable: Bool {
guard !self.context.isFrozen else {
return false
@@ -6600,15 +6632,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private final class ChatListTabBarContextReferenceContentSource: ContextReferenceContentSource {
let keepInPlace: Bool = true
let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
-
+
private let controller: ChatListController
private let sourceView: ContextExtractedContentContainingView
-
+
init(controller: ChatListController, sourceView: ContextExtractedContentContainingView) {
self.controller = controller
self.sourceView = sourceView
}
-
+
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(
referenceView: self.sourceView.contentView,
@@ -6621,15 +6653,15 @@ private final class ChatListTabBarContextReferenceContentSource: ContextReferenc
private final class ChatListHeaderBarContextReferenceContentSource: ContextReferenceContentSource {
let keepInPlace: Bool = true
let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
-
+
private let controller: ChatListController
private let sourceView: ContextExtractedContentContainingView
-
+
init(controller: ChatListController, sourceView: ContextExtractedContentContainingView) {
self.controller = controller
self.sourceView = sourceView
}
-
+
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(
referenceView: self.sourceView.contentView,
@@ -6643,18 +6675,18 @@ private final class ChatListHeaderBarContextExtractedContentSource: ContextExtra
let keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool = true
-
+
private let controller: ChatListController
private let sourceNode: ContextExtractedContentContainingNode?
private let sourceView: ContextExtractedContentContainingView?
-
+
init(controller: ChatListController, sourceNode: ContextExtractedContentContainingNode?, sourceView: ContextExtractedContentContainingView?, keepInPlace: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.sourceView = sourceView
self.keepInPlace = keepInPlace
}
-
+
func takeView() -> ContextControllerTakeViewInfo? {
if let sourceNode = self.sourceNode {
return ContextControllerTakeViewInfo(containingItem: .node(sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
@@ -6662,21 +6694,21 @@ private final class ChatListHeaderBarContextExtractedContentSource: ContextExtra
return ContextControllerTakeViewInfo(containingItem: .view(self.sourceView!), contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
-
+
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
-private final class ChatListContextLocationContentSource: ContextLocationContentSource {
+private final class ChatListContextLocationContentSource: ContextLocationContentSource {
private let controller: ViewController
private let location: CGPoint
-
+
init(controller: ViewController, location: CGPoint) {
self.controller = controller
self.location = location
}
-
+
func transitionInfo() -> ContextControllerLocationViewInfo? {
return ContextControllerLocationViewInfo(location: self.location, contentAreaInScreenSpace: UIScreen.main.bounds)
}
@@ -6700,21 +6732,21 @@ private final class ChatListLocationContext {
let context: AccountContext
let location: ChatListControllerLocation
weak var parentController: ChatListControllerImpl?
-
+
private var proxyUnavailableTooltipController: TooltipController?
private var didShowProxyUnavailableTooltipController = false
-
+
private var titleDisposable: Disposable?
-
+
private(set) var title: String = ""
private(set) var chatTitleComponent: ChatTitleComponent?
private(set) var chatListTitle: NetworkStatusTitle?
-
+
var leftButton: AnyComponentWithIdentity?
var rightButton: AnyComponentWithIdentity?
var proxyButton: AnyComponentWithIdentity?
var storyButton: AnyComponentWithIdentity?
-
+
var rightButtons: [AnyComponentWithIdentity] {
var result: [AnyComponentWithIdentity] = []
if let rightButton = self.rightButton {
@@ -6728,16 +6760,16 @@ private final class ChatListLocationContext {
}
return result
}
-
+
private(set) var toolbar: Toolbar?
-
+
private let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil)
-
+
private var didSetReady: Bool = false
let ready = Promise()
-
+
private var stateDisposable: Disposable?
-
+
init(
context: AccountContext,
location: ChatListControllerLocation,
@@ -6750,7 +6782,7 @@ private final class ChatListLocationContext {
self.context = context
self.location = location
self.parentController = parentController
-
+
let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> (Bool, Bool) in
if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
@@ -6762,13 +6794,13 @@ private final class ChatListLocationContext {
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
-
+
let passcode = context.sharedContext.accountManager.accessChallengeData()
|> map { view -> (Bool, Bool) in
let data = view.data
return (data.isLockable, false)
}
-
+
let peerStatus: Signal
switch self.location {
case .chatList(.root):
@@ -6789,7 +6821,7 @@ private final class ChatListLocationContext {
default:
peerStatus = .single(nil)
}
-
+
let networkState: Signal
#if DEBUG && false
networkState = .single(AccountNetworkState.connecting(proxy: nil)) |> then(.single(AccountNetworkState.updating(proxy: nil)) |> delay(2.0, queue: .mainQueue())) |> then(.single(AccountNetworkState.online(proxy: nil)) |> delay(2.0, queue: .mainQueue())) |> then(.complete() |> delay(2.0, queue: .mainQueue())) |> restart
@@ -6820,13 +6852,13 @@ private final class ChatListLocationContext {
|> deliverOnMainQueue).start(next: { value in
subscriber.putNext(value)
})
-
+
return ActionDisposable {
disposable.dispose()
}
}
#endif
-
+
switch location {
case .chatList:
if !hideNetworkActivityStatus {
@@ -6843,7 +6875,7 @@ private final class ChatListLocationContext {
guard let self else {
return
}
-
+
self.updateChatList(
networkState: networkState,
proxy: proxy,
@@ -6859,12 +6891,12 @@ private final class ChatListLocationContext {
self.didSetReady = true
self.ready.set(.single(true))
}
- case let .forum(peerId):
+ case let .forum(peerId):
let peerView = Promise()
peerView.set(context.account.viewTracker.peerView(peerId))
-
+
var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil))
-
+
let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get()
|> map { view -> Bool? in
if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel {
@@ -6898,7 +6930,7 @@ private final class ChatListLocationContext {
}
}
onlineMemberCount = recentOnlineSignal
-
+
self.titleDisposable = (combineLatest(queue: Queue.mainQueue(),
peerView.get(),
onlineMemberCount,
@@ -6921,7 +6953,7 @@ private final class ChatListLocationContext {
self.didSetReady = true
self.ready.set(.single(true))
}
-
+
let context = self.context
let location = self.location
let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set, Set)?, NoError> = containerNode.currentItemState
@@ -6959,12 +6991,12 @@ private final class ChatListLocationContext {
case .savedMessagesChats:
return .single(nil)
}
-
+
} else {
return .single(nil)
}
}
-
+
let peerView: Signal
if case let .forum(peerId) = location {
peerView = context.account.viewTracker.peerView(peerId)
@@ -6972,7 +7004,7 @@ private final class ChatListLocationContext {
} else {
peerView = .single(nil)
}
-
+
let previousToolbarValue = Atomic(value: nil)
self.stateDisposable = combineLatest(queue: .mainQueue(),
parentController.updatedPresentationData.1,
@@ -7046,7 +7078,7 @@ private final class ChatListLocationContext {
case .group:
actionTitle = presentationData.strings.Group_JoinGroup
}
-
+
}
toolbar = Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: actionTitle, isEnabled: true))
}
@@ -7065,12 +7097,12 @@ private final class ChatListLocationContext {
}
})
}
-
+
deinit {
self.titleDisposable?.dispose()
self.stateDisposable?.dispose()
}
-
+
private func updateChatList(
networkState: AccountNetworkState,
proxy: (Bool, Bool),
@@ -7095,9 +7127,9 @@ private final class ChatListLocationContext {
defaultTitle = ""
}
let previousEditingAndNetworkState = self.previousEditingAndNetworkStateValue.swap((stateAndFilterId.state.editing, networkState))
-
+
var titleContent: NetworkStatusTitle
-
+
if stateAndFilterId.state.editing {
if case .chatList(.root) = self.location {
self.rightButton = nil
@@ -7105,7 +7137,7 @@ private final class ChatListLocationContext {
self.proxyButton = nil
}
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle
-
+
var animated = false
if let (previousEditing, previousNetworkState) = previousEditingAndNetworkState {
if previousEditing != stateAndFilterId.state.editing, previousNetworkState == networkState, case .online = networkState {
@@ -7126,9 +7158,9 @@ private final class ChatListLocationContext {
let _ = self?.parentController?.reorderingDonePressed()
}
)))
-
+
let (_, connectsViaProxy) = proxy
-
+
switch networkState {
case .waitingForNetwork:
titleContent = NetworkStatusTitle(text: presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus)
@@ -7148,7 +7180,7 @@ private final class ChatListLocationContext {
var isRoot = false
if case .chatList(.root) = self.location {
isRoot = true
-
+
if isReorderingTabs {
self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
@@ -7164,7 +7196,7 @@ private final class ChatListLocationContext {
}
)))
}
-
+
if isReorderingTabs {
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
@@ -7189,7 +7221,7 @@ private final class ChatListLocationContext {
)))
}
}
-
+
if storyPostingAvailable {
self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent(
content: .icon(imageName: "Chat List/AddStoryIcon"),
@@ -7197,7 +7229,7 @@ private final class ChatListLocationContext {
guard let self, let parentController = self.parentController else {
return
}
-
+
if let componentView = parentController.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), storyPeerListView.isLiveStreaming {
parentController.displayContinueLiveStream()
} else {
@@ -7225,7 +7257,7 @@ private final class ChatListLocationContext {
}
)))
}
-
+
let (hasProxy, connectsViaProxy) = proxy
let (isPasscodeSet, isManuallyLocked) = passcode
var checkProxy = false
@@ -7243,7 +7275,7 @@ private final class ChatListLocationContext {
case .online:
titleContent = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus)
}
-
+
if titleContent.hasProxy {
let proxyStatus: ChatTitleProxyStatus
if titleContent.connectsViaProxy {
@@ -7251,7 +7283,7 @@ private final class ChatListLocationContext {
} else {
proxyStatus = .available
}
-
+
self.proxyButton = AnyComponentWithIdentity(id: "proxy", component: AnyComponent(NavigationButtonComponent(
content: .proxy(status: proxyStatus),
pressed: { [weak self] _ in
@@ -7261,15 +7293,15 @@ private final class ChatListLocationContext {
(parentController.navigationController as? NavigationController)?.pushViewController(self.context.sharedContext.makeProxySettingsController(context: self.context))
}
)))
-
+
titleContent.hasProxy = false
titleContent.connectsViaProxy = false
} else {
self.proxyButton = nil
}
-
+
self.chatListTitle = titleContent
-
+
if case .chatList(.root) = self.location, checkProxy {
if self.proxyUnavailableTooltipController == nil, !self.didShowProxyUnavailableTooltipController, let parentController = self.parentController, parentController.isNodeLoaded, parentController.displayNode.view.window != nil, parentController.navigationController?.topViewController == nil {
self.didShowProxyUnavailableTooltipController = true
@@ -7295,14 +7327,14 @@ private final class ChatListLocationContext {
}
}
}
-
+
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
-
+
self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring))
-
+
Queue.mainQueue().after(1.0, { [weak self] in
guard let self else {
return
@@ -7310,7 +7342,7 @@ private final class ChatListLocationContext {
self.parentController?.maybeDisplayStoryTooltip()
})
}
-
+
private func updateForum(
peerId: EnginePeer.Id,
peerView: PeerView,
@@ -7369,7 +7401,7 @@ private final class ChatListLocationContext {
}
)
}
-
+
if stateAndFilterId.state.editing {
self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
@@ -7396,12 +7428,12 @@ private final class ChatListLocationContext {
}
)))
}
-
+
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
-
+
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.isForumOrMonoForum {
if let parentController = self.parentController, let navigationController = parentController.navigationController as? NavigationController {
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
@@ -7411,7 +7443,7 @@ private final class ChatListLocationContext {
self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring))
}
}
-
+
private func performMoreAction(sourceView: UIView) {
guard let parentController = self.parentController else {
return
@@ -7434,14 +7466,14 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten
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)
}
@@ -7461,10 +7493,10 @@ public func resolveChatListNavigationTarget(navigationController: NavigationCont
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 61cbefe701..9b3e31bc6b 100644
--- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift
+++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift
@@ -33,7 +33,7 @@ import GlassControls
public enum ChatListContainerNodeFilter: Equatable {
case all
case filter(ChatListFilter)
-
+
public var id: ChatListFilterTabEntryId {
switch self {
case .all:
@@ -42,7 +42,7 @@ public enum ChatListContainerNodeFilter: Equatable {
return .filter(filter.id)
}
}
-
+
public var filter: ChatListFilter? {
switch self {
case .all:
@@ -65,16 +65,16 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
private let filterEmptyAction: (ChatListFilter?) -> Void
private let secondaryEmptyAction: () -> Void
private let openArchiveSettings: () -> Void
-
+
fileprivate var onStoriesLockedUpdated: ((Bool) -> Void)?
-
+
fileprivate var onFilterSwitch: (() -> Void)?
-
+
private var presentationData: PresentationData
-
+
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
-
+
private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:]
private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)?
private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] {
@@ -86,10 +86,10 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
var availableFiltersSignal: Signal<[ChatListContainerNodeFilter], NoError> {
return self.availableFiltersPromise.get()
}
-
+
private var filtersLimit: Int32? = nil
private var selectedId: ChatListFilterTabEntryId
-
+
var hintUpdatedStoryExpansion: Bool = false
var ignoreStoryUnlockedScrolling: Bool = false
var tempTopInset: CGFloat = 0.0 {
@@ -102,51 +102,51 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
var initialScrollingOffset: CGFloat?
-
+
public private(set) var transitionFraction: CGFloat = 0.0
private var transitionFractionOffset: CGFloat = 0.0
private var disableItemNodeOperationsWhileAnimating: Bool = false
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, insets: UIEdgeInsets, isReorderingFilters: Bool, isEditing: Bool, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
-
+
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
-
+
private var enableAdjacentFilterLoading: Bool = false
-
+
private var panRecognizer: InteractiveTransitionGestureRecognizer?
-
+
let leftSeparatorLayer: SimpleLayer
-
+
private let _ready = Promise()
public var ready: Signal {
return _ready.get()
}
-
+
private let _validLayoutReady = Promise()
var validLayoutReady: Signal {
return _validLayoutReady.get()
}
-
+
private var currentItemNodeValue: ChatListContainerItemNode?
public var currentItemNode: ChatListNode {
return self.currentItemNodeValue!.listNode
}
-
+
private let currentItemStateValue = Promise<(state: ChatListNodeState, filterId: Int32?)>()
var currentItemState: Signal<(state: ChatListNodeState, filterId: Int32?), NoError> {
return self.currentItemStateValue.get()
}
-
+
public var currentItemFilterUpdated: ((ChatListFilterTabEntryId, CGFloat, ContainedViewLayoutTransition, Bool) -> Void)?
public private(set) var isSwitchingCurrentItemFilterByDragging: Bool = false
public var currentItemFilter: ChatListFilterTabEntryId {
return self.currentItemNode.chatListFilter.flatMap { .filter($0.id) } ?? .all
}
-
+
private var didSetupContentOffset = false
private var isSettingUpContentOffset = false
-
+
private func applyItemNodeAsCurrent(id: ChatListFilterTabEntryId, itemNode: ChatListContainerItemNode) {
if let previousItemNode = self.currentItemNodeValue {
previousItemNode.listNode.activateSearch = nil
@@ -175,12 +175,12 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
previousItemNode.listNode.addedVisibleChatsWithPeerIds = nil
previousItemNode.listNode.didBeginSelectingChats = nil
previousItemNode.listNode.canExpandHiddenItems = nil
-
+
previousItemNode.accessibilityElementsHidden = true
}
self.currentItemNodeValue = itemNode
itemNode.accessibilityElementsHidden = false
-
+
itemNode.listNode.activateSearch = { [weak self] in
self?.activateSearch?()
}
@@ -233,22 +233,22 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
if self.isSettingUpContentOffset {
return
}
-
+
if !self.didSetupContentOffset, let initialScrollingOffset = self.initialScrollingOffset {
self.initialScrollingOffset = nil
self.didSetupContentOffset = true
self.isSettingUpContentOffset = true
-
+
let _ = itemNode.listNode.scrollToOffsetFromTop(initialScrollingOffset, animated: false)
-
+
let offset = itemNode.listNode.visibleContentOffset()
self.contentOffset = offset
self.contentOffsetChanged?(offset, self.currentItemNode)
-
+
self.isSettingUpContentOffset = false
return
}
-
+
if !self.isInlineMode, itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 {
if case let .known(value) = offset {
if value < -1.0 {
@@ -259,10 +259,10 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
self.contentOffset = offset
self.contentOffsetChanged?(offset, self.currentItemNode)
-
+
if !self.isInlineMode, self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset != 0.0 {
if case let .known(value) = offset {
if value > 4.0 {
@@ -281,17 +281,17 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
guard let self else {
return
}
-
+
self.didBeginInteractiveDragging?(listView)
-
+
if self.isInlineMode {
return
}
-
+
guard let validLayout = self.validLayout else {
return
}
-
+
let tempTopInset: CGFloat
if validLayout.inlineNavigationLocation != nil {
tempTopInset = 0.0
@@ -327,7 +327,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
guard let self else {
return false
}
-
+
return self.contentScrollingEnded?(listView) ?? false
}
itemNode.listNode.pinnedHeaderDisplayFractionUpdated = { [weak self] transition in
@@ -372,7 +372,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
itemNode.listNode.openAccountFreezeInfo = { [weak self] in
self?.openAccountFreezeInfo?()
}
-
+
self.currentItemStateValue.set(itemNode.listNode.state |> map { state in
let filterId: Int32?
switch id {
@@ -383,7 +383,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
return (state, filterId)
})
-
+
let enablePreload = context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]))
|> map { sharedData -> Bool in
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
@@ -395,7 +395,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
return automaticMediaDownloadSettings.energyUsageSettings.autodownloadInBackground
}
|> distinctUntilChanged
-
+
if self.controlsHistoryPreload, case .chatList(groupId: .root) = self.location {
self.context.account.viewTracker.chatListPreloadItems.set(combineLatest(queue: .mainQueue(),
context.sharedContext.enablePreloads.get(),
@@ -411,7 +411,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
})
}
}
-
+
public var activateSearch: (() -> Void)?
var presentAlert: ((String) -> Void)?
var present: ((ViewController) -> Void)?
@@ -446,7 +446,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
var didBeginSelectingChats: (() -> Void)?
var canExpandHiddenItems: (() -> Bool)?
public var displayFilterLimit: (() -> Void)?
-
+
public var pinnedHeaderDisplayFraction: CGFloat {
guard let currentItemNodeValue = self.currentItemNodeValue else {
return 0.0
@@ -464,7 +464,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
if let nextItemNode {
let absTransitionFraction = abs(self.transitionFraction)
return (1.0 - absTransitionFraction) * currentItemNodeValue.listNode.pinnedScrollFraction + absTransitionFraction * nextItemNode.listNode.pinnedScrollFraction
@@ -474,7 +474,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
return currentItemNodeValue.listNode.pinnedScrollFraction
}
-
+
public init(
context: AccountContext,
controller: ChatListControllerImpl?,
@@ -502,21 +502,21 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
self.secondaryEmptyAction = secondaryEmptyAction
self.openArchiveSettings = openArchiveSettings
self.controlsHistoryPreload = controlsHistoryPreload
-
+
self.presentationData = presentationData
self.animationCache = animationCache
self.animationRenderer = animationRenderer
-
+
self.selectedId = .all
-
+
self.leftSeparatorLayer = SimpleLayer()
self.leftSeparatorLayer.isHidden = true
self.leftSeparatorLayer.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor.cgColor
-
+
super.init()
-
+
self.backgroundColor = presentationData.theme.chatList.backgroundColor
-
+
let itemNode = ChatListContainerItemNode(context: self.context, controller: self.controller, location: self.location, filter: nil, chatListMode: chatListMode, previewing: self.previewing, isInlineMode: self.isInlineMode, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in
self?.filterBecameEmpty(filter)
}, emptyAction: { [weak self] filter in
@@ -528,11 +528,11 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}, autoSetReady: true, isMainTab: nil)
self.itemNodes[.all] = itemNode
self.addSubnode(itemNode)
-
+
self._ready.set(itemNode.listNode.ready)
-
+
self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode)
-
+
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let self, self.availableFilters.count > 1 || (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false)) else {
return []
@@ -562,18 +562,18 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer)
-
+
self.view.layer.addSublayer(self.leftSeparatorLayer)
}
-
+
deinit {
self.pendingItemNode?.2.dispose()
}
-
+
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
-
+
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
@@ -583,29 +583,29 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
return false
}
-
+
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count)
let maxFilterIndex = min(Int(filtersLimit), self.availableFilters.count) - 1
-
+
switch recognizer.state {
case .began:
self.onFilterSwitch?()
-
+
self.transitionFractionOffset = 0.0
if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] {
for (id, itemNode) in self.itemNodes {
if id != selectedId {
itemNode.emptyNode?.restartAnimation()
-
+
if let controller = self.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let scrollOffset = clippedScrollOffset
-
+
let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
}
}
}
-
+
if let presentationLayer = itemNode.layer.presentation() {
self.transitionFraction = presentationLayer.frame.minX / layout.size.width
self.transitionFractionOffset = self.transitionFraction
@@ -624,31 +624,31 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
var transitionFraction = translation.x / layout.size.width
-
+
var transition: ContainedViewLayoutTransition = .immediate
-
+
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 600.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
-
+
var hasLiveStream = false
if let componentView = self.controller?.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), storyPeerListView.isLiveStreaming {
hasLiveStream = true
}
-
+
if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) {
if hasLiveStream {
if translation.x >= 30.0 {
self.panRecognizer?.cancel()
-
+
self.controller?.displayContinueLiveStream()
}
return
}
-
+
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if selectedIndex <= 0 && translation.x > 0.0 {
transitionFraction = 0.0
@@ -656,7 +656,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
}
-
+
if cameraIsAlreadyOpened {
transitionFraction = 0.0
return
@@ -667,17 +667,17 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
}
}
-
+
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
let overscroll = -translation.x
transitionFraction = -rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
-
+
if let filtersLimit = self.filtersLimit, selectedIndex >= filtersLimit - 1 {
transitionFraction = 0.0
self.transitionFractionOffset = 0.0
recognizer.isEnabled = false
recognizer.isEnabled = true
-
+
transition = .animated(duration: 0.45, curve: .spring)
self.displayFilterLimit?()
}
@@ -720,13 +720,13 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
directionIsToRight = translation.x > layout.size.width / 2.0
}
}
-
+
let hasStoryCameraTransition = self.controller?.hasStoryCameraTransition ?? false
if hasStoryCameraTransition {
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
}
var applyNodeAsCurrent: ChatListFilterTabEntryId?
-
+
if let directionIsToRight = directionIsToRight {
var updatedIndex = selectedIndex
if directionIsToRight {
@@ -751,7 +751,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
}
}
-
+
if let switchToId = applyNodeAsCurrent, let itemNode = self.itemNodes[switchToId] {
self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode)
}
@@ -763,14 +763,14 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
break
}
}
-
+
func fixContentOffset(offset: CGFloat) {
self.currentItemNode.fixContentOffset(offset: offset)
}
-
+
public func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
-
+
if let validLayout = self.validLayout {
if let _ = validLayout.inlineNavigationLocation {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor.mixedWith(self.presentationData.theme.chatList.pinnedItemBackgroundColor, alpha: validLayout.inlineNavigationTransitionFraction)
@@ -778,14 +778,14 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
}
}
-
+
self.leftSeparatorLayer.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor.cgColor
-
+
for (_, itemNode) in self.itemNodes {
itemNode.updatePresentationData(presentationData)
}
}
-
+
func playArchiveAnimation() {
if let itemNode = self.itemNodes[self.selectedId] {
itemNode.listNode.forEachVisibleItemNode { node in
@@ -795,19 +795,19 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
public func scrollToTop(animated: Bool, adjustForTempInset: Bool) {
if let itemNode = self.itemNodes[self.selectedId] {
itemNode.listNode.scrollToPosition(.top(adjustForTempInset: adjustForTempInset), animated: animated)
}
}
-
+
func updateSelectedChatLocation(data: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
for (_, itemNode) in self.itemNodes {
itemNode.listNode.updateSelectedChatLocation(data, progress: progress, transition: transition)
}
}
-
+
func updateState(onlyCurrent: Bool = true, _ f: (ChatListNodeState) -> ChatListNodeState) {
self.currentItemNode.updateState(f)
let updatedState = self.currentItemNode.currentState
@@ -826,7 +826,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
public func updateAvailableFilters(_ availableFilters: [ChatListContainerNodeFilter], limit: Int32?) {
if self.availableFilters != availableFilters {
let apply: () -> Void = { [weak self] in
@@ -848,17 +848,17 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
public func updateEnableAdjacentFilterLoading(_ value: Bool) {
if value != self.enableAdjacentFilterLoading {
self.enableAdjacentFilterLoading = value
-
+
if self.enableAdjacentFilterLoading, let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
}
}
}
-
+
public func switchToFilter(id: ChatListFilterTabEntryId, animated: Bool = true, completion: (() -> Void)? = nil) {
self.onFilterSwitch?()
if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) {
@@ -866,13 +866,13 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout else {
return
}
-
+
if let controller = self.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let scrollOffset = clippedScrollOffset
-
+
let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
}
-
+
self.selectedId = id
self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
@@ -894,54 +894,54 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
self.pendingItemNode?.2.dispose()
let disposable = MetaDisposable()
self.pendingItemNode = (id, itemNode, disposable)
-
+
if !animated {
self.selectedId = id
self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false)
}
-
+
disposable.set((itemNode.listNode.ready
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
return
}
-
+
strongSelf.pendingItemNode?.2.dispose()
strongSelf.pendingItemNode = nil
itemNode.listNode.tempTopInset = strongSelf.tempTopInset
-
+
if let controller = strongSelf.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let scrollOffset = clippedScrollOffset
-
+
let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
}
-
+
guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = strongSelf.validLayout else {
strongSelf.itemNodes[id] = itemNode
strongSelf.addSubnode(itemNode)
-
+
strongSelf.selectedId = id
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, .immediate, false)
strongSelf.pinnedHeaderDisplayFractionUpdated?(.immediate)
-
+
completion?()
return
}
-
+
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
let previousId = strongSelf.selectedId
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0
let offset = offsetDirection * layout.size.width
-
+
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, index - 1) ... min(strongSelf.availableFilters.count - 1, index + 1) {
validNodeIds.append(strongSelf.availableFilters[i].id)
}
-
+
var removeIds: [ChatListFilterTabEntryId] = []
for (id, _) in strongSelf.itemNodes {
if !validNodeIds.contains(id) {
@@ -959,38 +959,38 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
strongSelf.itemNodes[id] = itemNode
strongSelf.addSubnode(itemNode)
-
+
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
itemNode.frame = itemFrame
-
+
transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: -offset, y: 0.0))
-
+
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
if let scrollingOffset = strongSelf.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
-
+
strongSelf.selectedId = id
if let currentItemNode = strongSelf.currentItemNodeValue {
itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden)
}
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
-
+
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
-
+
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false)
strongSelf.pinnedHeaderDisplayFractionUpdated?(transition)
}
-
+
completion?()
}))
-
+
if let (layout, _, visualNavigationHeight, originalNavigationHeight, _, insets, _, _, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
-
+
if let scrollingOffset = self.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
@@ -999,38 +999,38 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
}
-
+
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.scrollingOffset = (navigationHeight, offset)
for (_, itemNode) in self.itemNodes {
itemNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
}
}
-
+
public func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, insets: UIEdgeInsets, isReorderingFilters: Bool, isEditing: Bool, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
-
+
self._validLayoutReady.set(.single(true))
-
+
transition.updateAlpha(node: self, alpha: isReorderingFilters ? 0.5 : 1.0)
self.isUserInteractionEnabled = !isReorderingFilters
-
+
if let _ = inlineNavigationLocation {
transition.updateBackgroundColor(node: self, color: self.presentationData.theme.chatList.backgroundColor.mixedWith(self.presentationData.theme.chatList.pinnedItemBackgroundColor, alpha: inlineNavigationTransitionFraction))
} else {
transition.updateBackgroundColor(node: self, color: self.presentationData.theme.chatList.backgroundColor)
}
-
+
self.panRecognizer?.isEnabled = !isEditing
-
+
transition.updateFrame(layer: self.leftSeparatorLayer, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
-
+
if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) {
let id = self.availableFilters[i].id
validNodeIds.append(id)
-
+
if self.itemNodes[id] == nil && self.enableAdjacentFilterLoading && !self.disableItemNodeOperationsWhileAnimating {
let itemNode = ChatListContainerItemNode(context: self.context, controller: self.controller, location: self.location, filter: self.availableFilters[i].filter, chatListMode: self.chatListMode, previewing: self.previewing, isInlineMode: self.isInlineMode, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in
self?.filterBecameEmpty(filter)
@@ -1045,7 +1045,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
self.itemNodes[id] = itemNode
}
}
-
+
var removeIds: [ChatListFilterTabEntryId] = []
var animateSlidingIds: [ChatListFilterTabEntryId] = []
var slidingOffset: CGFloat?
@@ -1057,34 +1057,34 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
continue
}
let indexDistance = CGFloat(index - selectedIndex) + self.transitionFraction
-
+
let wasAdded = itemNode.supernode == nil
var nodeTransition = transition
if wasAdded {
self.addSubnode(itemNode)
nodeTransition = .immediate
}
-
+
let itemFrame = CGRect(origin: CGPoint(x: indexDistance * layout.size.width, y: 0.0), size: layout.size)
if !wasAdded && slidingOffset == nil {
slidingOffset = itemNode.frame.minX - itemFrame.minX
}
nodeTransition.updateFrame(node: itemNode, frame: itemFrame, completion: { _ in
})
-
+
var itemInlineNavigationTransitionFraction = inlineNavigationTransitionFraction
if indexDistance != 0 {
if itemInlineNavigationTransitionFraction != 0.0 || itemInlineNavigationTransitionFraction != 1.0 {
itemInlineNavigationTransitionFraction = itemNode.validLayout?.inlineNavigationTransitionFraction ?? 0.0
}
}
-
+
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0)
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
if let scrollingOffset = self.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition)
}
-
+
if wasAdded, case .animated = transition {
animateSlidingIds.append(id)
}
@@ -1114,9 +1114,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
private var presentationData: PresentationData
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
-
+
let mainContainerNode: ChatListContainerNode
-
+
var effectiveContainerNode: ChatListContainerNode {
if let inlineStackContainerNode = self.inlineStackContainerNode {
return inlineStackContainerNode
@@ -1124,44 +1124,44 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
return self.mainContainerNode
}
}
-
+
private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
private(set) var inlineStackContainerNode: ChatListContainerNode?
private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
-
+
private var tapRecognizer: UITapGestureRecognizer?
var navigationBar: NavigationBar?
let navigationBarView = ComponentView()
weak var controller: ChatListControllerImpl?
-
+
private var toolbar: ComponentView?
var toolbarData: Toolbar?
var toolbarActionSelected: ((ToolbarActionOption) -> Void)?
-
+
private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch?
private var skipSearchDisplayControllerLayout: Bool = false
private(set) var searchDisplayController: SearchDisplayController?
private var disappearingSearchDisplayController: SearchDisplayController?
-
+
var isReorderingFilters: Bool = false
var didBeginSelectingChatsWhileEditing: Bool = false
var isEditing: Bool = false
-
+
var tempAllowAvatarExpansion: Bool = false
private var tempDisableStoriesAnimations: Bool = false
private var tempNavigationScrollingTransition: ContainedViewLayoutTransition?
-
+
private var allowOverscrollStoryExpansion: Bool = false
private var currentOverscrollStoryExpansionTimestamp: Double?
-
+
private var allowOverscrollItemExpansion: Bool = false
private var currentOverscrollItemExpansionTimestamp: Double?
-
+
private var containerLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat)?
-
+
var contentScrollingEnded: ((ListView) -> Bool)?
-
+
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((EnginePeer, Int64?, Bool) -> Void)?
var requestOpenRecentPeerOptions: ((EnginePeer) -> Void)?
@@ -1173,16 +1173,16 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
var emptyListAction: ((EnginePeer.Id?) -> Void)?
var cancelEditing: (() -> Void)?
var dismissSearch: (() -> Void)?
-
+
let debugListView = ListViewImpl()
-
+
init(context: AccountContext, location: ChatListControllerLocation, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, controller: ChatListControllerImpl) {
self.context = context
self.location = location
self.presentationData = presentationData
self.animationCache = animationCache
self.animationRenderer = animationRenderer
-
+
var filterBecameEmpty: ((ChatListFilter?) -> Void)?
var filterEmptyAction: ((ChatListFilter?) -> Void)?
var secondaryEmptyAction: (() -> Void)?
@@ -1196,19 +1196,19 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}, openArchiveSettings: {
openArchiveSettings?()
})
-
+
self.controller = controller
-
+
super.init()
-
+
self.setViewBlock({
return UITracingLayerView()
})
-
+
self.backgroundColor = presentationData.theme.chatList.backgroundColor
-
+
self.addSubnode(self.mainContainerNode)
-
+
self.mainContainerNode.contentOffsetChanged = { [weak self] offset, listView in
self?.contentOffsetChanged(offset: offset, listView: listView, isPrimary: true)
}
@@ -1227,9 +1227,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.mainContainerNode.shouldStopScrolling = { [weak self] listView, velocity in
return self?.shouldStopScrolling(listView: listView, velocity: velocity, isPrimary: true) ?? false
}
-
+
self.addSubnode(self.debugListView)
-
+
filterBecameEmpty = { [weak self] _ in
guard let strongSelf = self else {
return
@@ -1244,29 +1244,29 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
strongSelf.emptyListAction?(nil)
}
-
+
secondaryEmptyAction = { [weak self] in
guard let strongSelf = self, case let .forum(peerId) = strongSelf.location, let controller = strongSelf.controller else {
return
}
-
+
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
(controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false)
}
-
+
openArchiveSettings = { [weak self] in
guard let self, let controller = self.controller else {
return
}
controller.push(self.context.sharedContext.makeArchiveSettingsController(context: self.context))
}
-
+
self.mainContainerNode.onFilterSwitch = { [weak self] in
if let strongSelf = self {
strongSelf.controller?.dismissAllUndoControllers()
}
}
-
+
self.mainContainerNode.onStoriesLockedUpdated = { [weak self] isLocked in
guard let self else {
return
@@ -1278,12 +1278,12 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.controller?.requestLayout(transition: .immediate)
}
}
-
+
self.mainContainerNode.canExpandHiddenItems = { [weak self] in
guard let self, let controller = self.controller else {
return false
}
-
+
if let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarComponentView.storiesUnlocked {
@@ -1295,7 +1295,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
return true
}
}
-
+
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else {
return []
@@ -1309,26 +1309,26 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.inlineContentPanRecognizer = inlineContentPanRecognizer
self.view.addGestureRecognizer(inlineContentPanRecognizer)
}
-
+
override func didLoad() {
super.didLoad()
-
+
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.view.addGestureRecognizer(tapRecognizer)
tapRecognizer.isEnabled = false
}
-
+
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancelEditing?()
}
}
-
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
-
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
@@ -1338,7 +1338,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
return false
}
-
+
@objc private func inlineContentPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
@@ -1375,7 +1375,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
directionIsToRight = translation.x > inlineStackContainerNode.bounds.width / 2.0
}
}
-
+
if let directionIsToRight = directionIsToRight, directionIsToRight {
self.controller?.setInlineChatList(location: nil)
} else {
@@ -1387,20 +1387,20 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
break
}
}
-
+
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
-
+
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
-
+
self.mainContainerNode.updatePresentationData(presentationData)
self.inlineStackContainerNode?.updatePresentationData(presentationData)
self.searchDisplayController?.updatePresentationData(presentationData)
}
-
+
private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
let headerContent = self.controller?.updateHeaderContent()
-
+
var panels: [HeaderPanelContainerComponent.Panel] = []
if let chatListNotice = self.controller?.globalControlPanelsContextState?.chatListNotice {
panels.append(HeaderPanelContainerComponent.Panel(
@@ -1468,7 +1468,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
if let mediaPlayback = self.controller?.globalControlPanelsContextState?.mediaPlayback {
if let playlistLocation = mediaPlayback.playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(_, _, _, _, hidePanel) = playlistLocation, hidePanel {
-
+
} else {
panels.append(HeaderPanelContainerComponent.Panel(
key: "media",
@@ -1500,7 +1500,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
)))
)
}
-
+
var navigationHeaderPanels: AnyComponent?
if self.controller?.tabContainerData != nil || !panels.isEmpty {
var tabs: AnyComponent?
@@ -1530,9 +1530,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
case let .filter(id):
selectedTab = AnyHashable(id)
}
-
+
let isEditing = self.isReorderingFilters || (self.mainContainerNode.currentItemNode.currentState.editing && !self.didBeginSelectingChatsWhileEditing)
-
+
tabs = AnyComponent(HorizontalTabsComponent(
context: self.context,
theme: self.presentationData.theme,
@@ -1556,7 +1556,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
)
}
}
-
+
return HorizontalTabsComponent.Tab(
id: id,
content: .title(title),
@@ -1565,18 +1565,18 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
guard let self, let tabContainerData = self.controller?.tabContainerData else {
return
}
-
+
let isPremium = self.context.isPremium
-
+
let mappedId: ChatListFilterTabEntryId = entry.id
-
+
var isDisabled = false
if let filtersLimit = tabContainerData.2 {
if let folderIndex = folderFilterIndex(mappedId, tabContainerData.0) {
isDisabled = !isPremium && folderIndex >= filtersLimit
}
}
-
+
if isDisabled {
let filtersCount = tabContainerData.0.count(where: { item in
if case .all = item {
@@ -1604,9 +1604,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
guard let self, let tabContainerData = self.controller?.tabContainerData else {
return
}
-
+
let isPremium = self.context.isPremium
-
+
let mappedId: Int32?
switch entry {
case .all:
@@ -1614,14 +1614,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
case let .filter(idValue, _, _):
mappedId = idValue
}
-
+
var isDisabled = false
if let filtersLimit = tabContainerData.2 {
if let folderIndex = folderFilterIndex(entry.id, tabContainerData.0) {
isDisabled = !isPremium && folderIndex >= filtersLimit
}
}
-
+
self.controller?.tabContextGesture(id: mappedId, sourceNode: nil, sourceView: sourceView, gesture: gesture, keepInPlace: false, isDisabled: isDisabled)
},
deleteAction: (!isEditing || isMainTab) ? nil : { [weak self] in
@@ -1639,14 +1639,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
liftWhileSwitching: layout.deviceMetrics.type != .tablet
))
}
-
+
navigationHeaderPanels = AnyComponent(HeaderPanelContainerComponent(
theme: self.presentationData.theme,
tabs: tabs,
panels: panels
))
}
-
+
var effectiveStorySubscriptions: EngineStorySubscriptions?
if let controller = self.controller, case .forum = controller.location {
effectiveStorySubscriptions = nil
@@ -1657,7 +1657,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
effectiveStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil)
}
}
-
+
let navigationBarSize = self.navigationBarView.update(
transition: transition,
component: AnyComponent(ChatListNavigationBar(
@@ -1683,14 +1683,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
guard let self, let controller = self.controller else {
return
}
-
+
var isForum = false
if case .forum = controller.location {
isForum = true
}
-
+
let filter: ChatListSearchFilter = isForum ? .topics : .chats
-
+
controller.activateSearch(
filter: filter,
query: nil,
@@ -1718,18 +1718,18 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
if deferScrollApplication {
navigationBarComponentView.deferScrollApplication = true
}
-
+
if navigationBarComponentView.superview == nil {
self.view.addSubview(navigationBarComponentView)
}
transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize))
-
+
return (navigationBarSize.height, 0.0)
} else {
return (0.0, 0.0)
}
}
-
+
private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var mainOffset: CGFloat
if let contentOffset = self.mainContainerNode.contentOffset, case let .known(value) = contentOffset {
@@ -1737,14 +1737,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
mainOffset = navigationHeight
}
-
+
self.mainContainerNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: mainOffset, transition: transition)
-
+
mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight)
if abs(mainOffset) < 0.1 {
mainOffset = 0.0
}
-
+
let resultingOffset: CGFloat
if let inlineStackContainerNode = self.inlineStackContainerNode {
var inlineOffset: CGFloat
@@ -1757,17 +1757,17 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
if abs(inlineOffset) < 0.1 {
inlineOffset = 0.0
}
-
+
resultingOffset = mainOffset * (1.0 - self.inlineStackContainerTransitionFraction) + inlineOffset * self.inlineStackContainerTransitionFraction
} else {
resultingOffset = mainOffset
}
-
+
var offset = resultingOffset
if self.isSearchDisplayControllerActive != nil {
offset = 0.0
}
-
+
var allowAvatarsExpansion: Bool = true
if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.tempAllowAvatarExpansion {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
@@ -1776,14 +1776,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
}
}
-
+
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: ComponentTransition(transition).withUserData(ChatListNavigationBar.AnimationHint(
disableStoriesAnimations: self.tempDisableStoriesAnimations,
crossfadeStoryPeers: false
)))
}
-
+
let mainDelta: CGFloat
if let _ = self.inlineStackContainerNode {
mainDelta = resultingOffset - max(0.0, mainOffset)
@@ -1792,22 +1792,22 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
transition.updateSublayerTransformOffset(layer: self.mainContainerNode.layer, offset: CGPoint(x: 0.0, y: -mainDelta))
}
-
+
func requestNavigationBarLayout(transition: ComponentTransition) {
guard let (layout, _, _, _, _) = self.containerLayout else {
return
}
let _ = self.updateNavigationBar(layout: layout, deferScrollApplication: false, transition: transition)
}
-
+
func scrollToStories(animated: Bool) {
if self.inlineStackContainerNode != nil {
return
}
-
+
if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
let _ = storySubscriptions
-
+
self.tempAllowAvatarExpansion = true
self.tempDisableStoriesAnimations = !animated
self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .custom(0.33, 0.52, 0.25, 0.99)) : .immediate
@@ -1817,28 +1817,28 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
tempNavigationScrollingTransition = nil
}
}
-
+
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
var navigationBarHeight = navigationBarHeight
var visualNavigationHeight = visualNavigationHeight
var cleanNavigationBarHeight = cleanNavigationBarHeight
var storiesInset = storiesInset
-
+
let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: ComponentTransition(transition))
self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset
-
+
navigationBarHeight = navigationBarLayout.navigationHeight
visualNavigationHeight = navigationBarLayout.navigationHeight
cleanNavigationBarHeight = navigationBarLayout.navigationHeight
storiesInset = navigationBarLayout.storiesInset
-
+
self.containerLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, storiesInset)
-
+
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
-
+
if let toolbarData = self.toolbarData {
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
@@ -1849,11 +1849,11 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
panelsBottomInset = max(panelsBottomInset, 8.0)
}
-
+
let sideInset: CGFloat = 20.0
let toolbarHeight = 44.0
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
-
+
let toolbar: ComponentView
var toolbarTransition = ComponentTransition(transition)
if let current = self.toolbar {
@@ -1863,7 +1863,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.toolbar = toolbar
toolbarTransition = .immediate
}
-
+
let _ = toolbar.update(
transition: toolbarTransition,
component: AnyComponent(GlassControlPanelComponent(
@@ -1918,7 +1918,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
environment: {},
containerSize: toolbarFrame.size
)
-
+
if let toolbarView = toolbar.view {
if toolbarView.superview == nil {
self.view.addSubview(toolbarView)
@@ -1935,11 +1935,11 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
})
}
}
-
+
var childrenLayout = layout
childrenLayout.intrinsicInsets = UIEdgeInsets(top: visualNavigationHeight, left: childrenLayout.intrinsicInsets.left, bottom: childrenLayout.intrinsicInsets.bottom, right: childrenLayout.intrinsicInsets.right)
self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition)
-
+
transition.updateFrame(node: self.mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var mainNavigationBarHeight = navigationBarHeight
var cleanMainNavigationBarHeight = cleanNavigationBarHeight
@@ -1950,7 +1950,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
mainInsets.top = visualNavigationHeight
}
self.mainContainerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, storiesInset: storiesInset, transition: transition)
-
+
if let inlineStackContainerNode = self.inlineStackContainerNode {
var inlineStackContainerNodeTransition = transition
var animateIn = false
@@ -1959,7 +1959,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
inlineStackContainerNodeTransition = .immediate
animateIn = true
}
-
+
let inlineSideInset: CGFloat = layout.safeInsets.left + 72.0
var inlineStackFrame = CGRect(origin: CGPoint(x: inlineSideInset, y: 0.0), size: CGSize(width: layout.size.width - inlineSideInset, height: layout.size.height))
inlineStackFrame.origin.x += (1.0 - self.inlineStackContainerTransitionFraction) * inlineStackFrame.width
@@ -1969,21 +1969,21 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
inlineLayout.safeInsets.left = 0.0
inlineLayout.intrinsicInsets.left = 0.0
inlineLayout.additionalInsets.left = 0.0
-
+
var inlineInsets = insets
inlineInsets.left = 0.0
-
+
let inlineNavigationHeight: CGFloat = navigationBarLayout.navigationHeight - navigationBarLayout.storiesInset
-
+
inlineStackContainerNode.update(layout: inlineLayout, navigationBarHeight: inlineNavigationHeight, visualNavigationHeight: inlineNavigationHeight, originalNavigationHeight: inlineNavigationHeight, cleanNavigationBarHeight: inlineNavigationHeight, insets: inlineInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0, storiesInset: storiesInset, transition: inlineStackContainerNodeTransition)
-
+
if animateIn {
transition.animatePosition(node: inlineStackContainerNode, from: CGPoint(x: inlineStackContainerNode.position.x + inlineStackContainerNode.bounds.width + UIScreenPixel, y: inlineStackContainerNode.position.y))
}
}
-
+
self.tapRecognizer?.isEnabled = self.isReorderingFilters
-
+
if let searchDisplayController = self.searchDisplayController {
if !self.skipSearchDisplayControllerLayout {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)
@@ -1992,28 +1992,28 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
if let disappearingSearchDisplayController = self.disappearingSearchDisplayController {
disappearingSearchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)
}
-
+
self.updateNavigationScrolling(navigationHeight: navigationBarLayout.navigationHeight, transition: transition)
-
+
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.deferScrollApplication = false
navigationBarComponentView.applyCurrentScroll(transition: ComponentTransition(transition))
}
}
-
+
@MainActor
func activateSearch(placeholderNode: SearchBarPlaceholderNode?, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?, searchBarIsExternal: Bool) async -> ((Bool) -> Void)? {
guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else {
return nil
}
-
+
let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location
-
+
let filter: ChatListNodePeersFilter = []
if case .forum = effectiveLocation {
//filter.insert(.excludeRecent)
}
-
+
var folder: (Int32, String)?
if let folders = self.controller?.tabContainerData?.0 {
switch self.effectiveContainerNode.currentItemFilter {
@@ -2027,7 +2027,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
}
}
-
+
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
@@ -2057,10 +2057,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
contentNode.openAdInfo = { [weak self] node, adPeer in
self?.controller?.openAdInfo(node: node, adPeer: adPeer)
}
-
+
let searchTips = await ApplicationSpecificNotice.getGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).get()
contentNode.displayGlobalPostsNewBadge = searchTips < 3
-
+
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()
@@ -2068,20 +2068,20 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}, fieldStyle: placeholderNode?.fieldStyle ?? .modern, searchBarIsExternal: searchBarIsExternal)
self.mainContainerNode.accessibilityElementsHidden = true
self.inlineStackContainerNode?.accessibilityElementsHidden = true
-
+
return ({ [weak self] focus in
guard let strongSelf = self else {
return
}
-
+
strongSelf.isSearchDisplayControllerActive = ChatListNavigationBar.ActiveSearch(isExternal: placeholderNode == nil)
-
+
strongSelf.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
strongSelf.searchDisplayController?.activate(insertSubnode: { [weak self] subnode, isSearchBar in
guard let self else {
return
}
-
+
if isSearchBar {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.searchContentNode?.addSubnode(subnode)
@@ -2090,11 +2090,11 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.insertSubnode(subnode, aboveSubnode: self.debugListView)
}
}, placeholder: placeholderNode, focus: focus)
-
+
strongSelf.controller?.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
})
}
-
+
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode?, animated: Bool) -> (() -> Void)? {
if let searchDisplayController = self.searchDisplayController {
self.isSearchDisplayControllerActive = nil
@@ -2102,7 +2102,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.disappearingSearchDisplayController = searchDisplayController
self.mainContainerNode.accessibilityElementsHidden = false
self.inlineStackContainerNode?.accessibilityElementsHidden = false
-
+
return { [weak self, weak placeholderNode] in
guard let self, let (layout, _, _, cleanNavigationBarHeight, _) = self.containerLayout else {
return
@@ -2116,35 +2116,35 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.disappearingSearchDisplayController = nil
}
})
-
+
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring))
-
+
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
}
} else {
return nil
}
}
-
+
func clearHighlightAnimated(_ animated: Bool) {
self.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
self.inlineStackContainerNode?.currentItemNode.clearHighlightAnimated(true)
}
-
+
private var contentOffsetSyncLockedIn: Bool = false
-
+
func willScrollToTop() {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .slide)))
}
}
-
+
private func contentOffsetChanged(offset: ListViewVisibleContentOffset, listView: ListView, isPrimary: Bool) {
guard let containerLayout = self.containerLayout else {
return
}
self.updateNavigationScrolling(navigationHeight: containerLayout.navigationBarHeight, transition: self.tempNavigationScrollingTransition ?? .immediate)
-
+
if listView.isDragging {
var overscrollSelectedId: EnginePeer.Id?
var overscrollHiddenChatItemsAllowed = false
@@ -2152,13 +2152,13 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
overscrollSelectedId = storyPeerListView.overscrollSelectedId
overscrollHiddenChatItemsAllowed = storyPeerListView.overscrollHiddenChatItemsAllowed
}
-
+
if let chatListNode = listView as? ChatListNode {
if chatListNode.hasItemsToBeRevealed() {
overscrollSelectedId = nil
}
}
-
+
if let controller = self.controller {
if let peerId = overscrollSelectedId {
if self.allowOverscrollStoryExpansion && self.inlineStackContainerNode == nil && isPrimary {
@@ -2167,21 +2167,21 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
self.currentOverscrollStoryExpansionTimestamp = timestamp
}
-
+
if let currentOverscrollStoryExpansionTimestamp = self.currentOverscrollStoryExpansionTimestamp, currentOverscrollStoryExpansionTimestamp <= timestamp - 0.0 {
self.allowOverscrollStoryExpansion = false
self.currentOverscrollStoryExpansionTimestamp = nil
self.allowOverscrollItemExpansion = false
self.currentOverscrollItemExpansionTimestamp = nil
HapticFeedback().tap()
-
+
controller.openStories(peerId: peerId)
}
}
} else {
if !overscrollHiddenChatItemsAllowed {
var manuallyAllow = false
-
+
if isPrimary {
if let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
} else {
@@ -2190,12 +2190,12 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
manuallyAllow = true
}
-
+
if manuallyAllow, case let .known(value) = offset, value + listView.tempTopInset <= -40.0 {
overscrollHiddenChatItemsAllowed = true
}
}
-
+
if overscrollHiddenChatItemsAllowed {
if self.allowOverscrollItemExpansion {
let timestamp = CACurrentMediaTime()
@@ -2203,10 +2203,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
self.currentOverscrollItemExpansionTimestamp = timestamp
}
-
+
if let currentOverscrollItemExpansionTimestamp = self.currentOverscrollItemExpansionTimestamp, currentOverscrollItemExpansionTimestamp <= timestamp - 0.0 {
self.allowOverscrollItemExpansion = false
-
+
if isPrimary {
self.mainContainerNode.currentItemNode.revealScrollHiddenItem()
} else {
@@ -2219,21 +2219,21 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
}
}
-
+
private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool {
if abs(velocity) > 0.8 {
return false
}
-
+
if !isPrimary || self.inlineStackContainerNode == nil {
} else {
return false
}
-
+
guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else {
return false
}
-
+
if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let searchScrollOffset = clippedScrollOffset
if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight {
@@ -2242,10 +2242,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
return true
}
}
-
+
return false
}
-
+
private func didBeginInteractiveDragging(listView: ListView, isPrimary: Bool) {
if isPrimary {
if let chatListNode = listView as? ChatListNode, !chatListNode.hasItemsToBeRevealed() {
@@ -2256,7 +2256,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
self.allowOverscrollItemExpansion = true
}
-
+
private func endedInteractiveDragging(listView: ListView, isPrimary: Bool) {
if isPrimary {
self.allowOverscrollStoryExpansion = false
@@ -2265,17 +2265,17 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.allowOverscrollItemExpansion = false
self.currentOverscrollItemExpansionTimestamp = nil
}
-
+
private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool {
if !isPrimary || self.inlineStackContainerNode == nil {
} else {
return false
}
-
+
guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else {
return false
}
-
+
if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let searchScrollOffset = clippedScrollOffset
if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight {
@@ -2294,10 +2294,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
return true
}
}
-
+
return false
}
-
+
private func pinnedHeaderDisplayFractionUpdated(transition: ContainedViewLayoutTransition) {
var pinnedFraction: CGFloat = 0.0
if let inlineStackContainerNode = self.inlineStackContainerNode, self.inlineStackContainerTransitionFraction != 0.0 {
@@ -2305,22 +2305,22 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} else {
pinnedFraction = self.mainContainerNode.pinnedHeaderDisplayFraction
}
-
+
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.updateEdgeEffectForPinnedFraction(pinnedFraction: pinnedFraction, transition: ComponentTransition(transition))
}
}
-
+
func makeInlineChatList(location: ChatListControllerLocation) -> ChatListContainerNode {
var forumPeerId: EnginePeer.Id?
if case let .forum(peerId) = location {
forumPeerId = peerId
}
-
+
let inlineStackContainerNode = ChatListContainerNode(context: self.context, controller: self.controller, location: location, previewing: false, controlsHistoryPreload: false, isInlineMode: true, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { [weak self] _ in self?.emptyListAction?(forumPeerId) }, secondaryEmptyAction: {}, openArchiveSettings: {})
return inlineStackContainerNode
}
-
+
func setInlineChatList(inlineStackContainerNode: ChatListContainerNode?) {
if let inlineStackContainerNode = inlineStackContainerNode {
if self.inlineStackContainerNode !== inlineStackContainerNode {
@@ -2329,9 +2329,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
let _ = self.mainContainerNode.currentItemNode.scrollToOffsetFromTop(self.mainContainerNode.currentItemNode.tempTopInset, animated: true)
}
}
-
+
inlineStackContainerNode.leftSeparatorLayer.isHidden = false
-
+
inlineStackContainerNode.presentAlert = self.mainContainerNode.presentAlert
inlineStackContainerNode.present = self.mainContainerNode.present
inlineStackContainerNode.push = self.mainContainerNode.push
@@ -2343,7 +2343,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
inlineStackContainerNode.peerSelected = self.mainContainerNode.peerSelected
inlineStackContainerNode.groupSelected = self.mainContainerNode.groupSelected
inlineStackContainerNode.updatePeerGrouping = self.mainContainerNode.updatePeerGrouping
-
+
inlineStackContainerNode.contentOffsetChanged = { [weak self] offset, listView in
self?.contentOffsetChanged(offset: offset, listView: listView, isPrimary: false)
}
@@ -2362,39 +2362,39 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
inlineStackContainerNode.pinnedHeaderDisplayFractionUpdated = { [weak self] transition in
self?.pinnedHeaderDisplayFractionUpdated(transition: transition)
}
-
+
inlineStackContainerNode.activateChatPreview = self.mainContainerNode.activateChatPreview
inlineStackContainerNode.openStories = self.mainContainerNode.openStories
inlineStackContainerNode.addedVisibleChatsWithPeerIds = self.mainContainerNode.addedVisibleChatsWithPeerIds
inlineStackContainerNode.didBeginSelectingChats = self.mainContainerNode.didBeginSelectingChats
inlineStackContainerNode.displayFilterLimit = nil
-
+
let previousInlineStackContainerNode = self.inlineStackContainerNode
-
+
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
let scrollOffset = max(0.0, clippedScrollOffset)
inlineStackContainerNode.initialScrollingOffset = scrollOffset
}
-
+
self.inlineStackContainerNode = inlineStackContainerNode
self.inlineStackContainerTransitionFraction = 1.0
-
+
if let _ = self.containerLayout {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
-
+
if let contentOffset = self.mainContainerNode.contentOffset, case let .known(offset) = contentOffset, offset < 0.0 {
if let containerLayout = self.containerLayout {
self.updateNavigationScrolling(navigationHeight: containerLayout.navigationBarHeight, transition: transition)
self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
}
}
-
+
if let previousInlineStackContainerNode {
transition.updatePosition(node: previousInlineStackContainerNode, position: CGPoint(x: previousInlineStackContainerNode.position.x + previousInlineStackContainerNode.bounds.width + UIScreenPixel, y: previousInlineStackContainerNode.position.y), completion: { [weak previousInlineStackContainerNode] _ in
previousInlineStackContainerNode?.removeFromSupernode()
})
}
-
+
self.controller?.requestLayout(transition: transition)
} else {
previousInlineStackContainerNode?.removeFromSupernode()
@@ -2404,14 +2404,14 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
if let inlineStackContainerNode = self.inlineStackContainerNode {
self.inlineStackContainerNode = nil
self.inlineStackContainerTransitionFraction = 0.0
-
+
if let _ = self.containerLayout {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
-
+
transition.updatePosition(node: inlineStackContainerNode, position: CGPoint(x: inlineStackContainerNode.position.x + inlineStackContainerNode.bounds.width + UIScreenPixel, y: inlineStackContainerNode.position.y), completion: { [weak inlineStackContainerNode] _ in
inlineStackContainerNode?.removeFromSupernode()
})
-
+
self.temporaryContentOffsetChangeTransition = transition
self.tempNavigationScrollingTransition = transition
self.controller?.requestLayout(transition: transition)
@@ -2423,11 +2423,11 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
}
}
-
+
func playArchiveAnimation() {
self.mainContainerNode.playArchiveAnimation()
}
-
+
func scrollToTop() {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.contentNode.scrollToTop()
@@ -2437,7 +2437,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
}
}
-
+
func scrollToTopIfStoriesAreExpanded() {
if let contentOffset = self.mainContainerNode.contentOffset, case let .known(offset) = contentOffset, offset < 0.0 {
self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
@@ -2447,6 +2447,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
}
func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions, isHidden: Bool) -> Bool {
+ if currentWinterGramSettings.disableStories {
+ return false
+ }
if !storySubscriptions.items.isEmpty {
return true
}
diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift
index 67257066fe..46f94ab43a 100644
--- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift
+++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift
@@ -5,6 +5,7 @@ import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
+import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AvatarNode
@@ -37,7 +38,7 @@ public enum ChatListItemContent {
public let isClosed: Bool
public let isHidden: Bool
public let threadPeer: EnginePeer?
-
+
public init(id: Int64, info: EngineMessageHistoryThread.Info, isOwnedByMe: Bool, isClosed: Bool, isHidden: Bool, threadPeer: EnginePeer?) {
self.id = id
self.info = info
@@ -46,7 +47,7 @@ public enum ChatListItemContent {
self.isHidden = isHidden
self.threadPeer = threadPeer
}
-
+
public static func ==(lhs: ThreadInfo, rhs: ThreadInfo) -> Bool {
if lhs.id != rhs.id {
return false
@@ -69,7 +70,7 @@ public enum ChatListItemContent {
return true
}
}
-
+
public final class DraftState: Equatable {
let text: String
let entities: [MessageTextEntity]
@@ -89,11 +90,11 @@ public enum ChatListItemContent {
return true
}
}
-
+
public struct StoryState: Equatable {
public var stats: EngineChatList.StoryStats
public var hasUnseenCloseFriends: Bool
-
+
public init(
stats: EngineChatList.StoryStats,
hasUnseenCloseFriends: Bool
@@ -102,19 +103,19 @@ public enum ChatListItemContent {
self.hasUnseenCloseFriends = hasUnseenCloseFriends
}
}
-
+
public struct Tag: Equatable {
public var id: Int32
public var title: ChatFolderTitle
public var colorId: Int32
-
+
public init(id: Int32, title: ChatFolderTitle, colorId: Int32) {
self.id = id
self.title = title
self.colorId = colorId
}
}
-
+
public struct CustomMessageListData: Equatable {
public var commandPrefix: String?
public var searchQuery: String?
@@ -123,7 +124,7 @@ public enum ChatListItemContent {
public var hideDate: Bool
public var hidePeerStatus: Bool
public var isInTransparentContainer: Bool
-
+
public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool, hideDate: Bool, hidePeerStatus: Bool, isInTransparentContainer: Bool = false) {
self.commandPrefix = commandPrefix
self.searchQuery = searchQuery
@@ -134,7 +135,7 @@ public enum ChatListItemContent {
self.isInTransparentContainer = isInTransparentContainer
}
}
-
+
public struct PeerData {
public var messages: [EngineMessage]
public var peer: EngineRenderedPeer
@@ -160,7 +161,7 @@ public enum ChatListItemContent {
public var displayAsTopicList: Bool
public var tags: [Tag]
public var customMessageListData: CustomMessageListData?
-
+
public init(
messages: [EngineMessage],
peer: EngineRenderedPeer,
@@ -213,7 +214,7 @@ public enum ChatListItemContent {
self.customMessageListData = customMessageListData
}
}
-
+
public struct GroupReferenceData {
public var groupId: EngineChatList.Group
public var peers: [EngineChatList.GroupItem.Item]
@@ -222,7 +223,7 @@ public enum ChatListItemContent {
public var hiddenByDefault: Bool
public var appearsPinned: Bool
public var storyState: StoryState?
-
+
public init(
groupId: EngineChatList.Group,
peers: [EngineChatList.GroupItem.Item],
@@ -245,7 +246,7 @@ public enum ChatListItemContent {
case loading
case peer(PeerData)
case groupReference(GroupReferenceData)
-
+
public var chatLocation: ChatLocation? {
switch self {
case .loading:
@@ -267,7 +268,7 @@ private final class ChatListItemTagListComponent: Component {
let tags: [ChatListItemContent.Tag]
let theme: PresentationTheme
let sizeFactor: CGFloat
-
+
init(
context: AccountContext,
tags: [ChatListItemContent.Tag],
@@ -279,7 +280,7 @@ private final class ChatListItemTagListComponent: Component {
self.theme = theme
self.sizeFactor = sizeFactor
}
-
+
static func ==(lhs: ChatListItemTagListComponent, rhs: ChatListItemTagListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
@@ -295,28 +296,28 @@ private final class ChatListItemTagListComponent: Component {
}
return true
}
-
+
private final class ItemView: UIView {
let backgroundView: UIImageView
let title = ComponentView()
-
+
private var currentTitle: ChatFolderTitle?
-
+
override init(frame: CGRect) {
self.backgroundView = UIImageView(image: tagBackgroundImage)
-
+
super.init(frame: frame)
-
+
self.addSubview(self.backgroundView)
}
-
+
required init?(coder: NSCoder) {
preconditionFailure()
}
-
+
func update(context: AccountContext, title: ChatFolderTitle, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize {
self.currentTitle = title
-
+
let titleValue = ChatFolderTitle(text: title.text.isEmpty ? " " : title.text, entities: title.entities, enableAnimations: title.enableAnimations)
let titleSize = self.title.update(
transition: .immediate,
@@ -332,15 +333,15 @@ private final class ChatListItemTagListComponent: Component {
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
-
+
let backgroundSideInset: CGFloat = floorToScreenPixels(4.0 * sizeFactor)
let backgroundVerticalInset: CGFloat = floorToScreenPixels(2.0 * sizeFactor)
let backgroundSize = CGSize(width: titleSize.width + backgroundSideInset * 2.0, height: titleSize.height + backgroundVerticalInset * 2.0)
-
+
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
self.backgroundView.frame = backgroundFrame
self.backgroundView.tintColor = backgroundColor
-
+
let titleFrame = titleSize.centered(in: backgroundFrame)
if let titleView = self.title.view {
if titleView.superview == nil {
@@ -348,10 +349,10 @@ private final class ChatListItemTagListComponent: Component {
}
titleView.frame = titleFrame
}
-
+
return backgroundSize
}
-
+
func updateVisibility(_ isVisible: Bool) {
guard let currentTitle = self.currentTitle else {
return
@@ -361,10 +362,10 @@ private final class ChatListItemTagListComponent: Component {
}
}
}
-
+
final class View: UIView {
private var itemViews: [Int32: ItemView] = [:]
-
+
var isVisible: Bool = false {
didSet {
if self.isVisible != oldValue {
@@ -374,15 +375,15 @@ private final class ChatListItemTagListComponent: Component {
}
}
}
-
+
override init(frame: CGRect) {
super.init(frame: frame)
}
-
+
required init?(coder: NSCoder) {
preconditionFailure()
}
-
+
func update(component: ChatListItemTagListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
var validIds: [Int32] = []
let spacing: CGFloat = floorToScreenPixels(5.0 * component.sizeFactor)
@@ -391,12 +392,12 @@ private final class ChatListItemTagListComponent: Component {
if nextX != 0.0 {
nextX += spacing
}
-
+
let itemId: Int32
let itemTitle: ChatFolderTitle
let itemBackgroundColor: UIColor
let itemForegroundColor: UIColor
-
+
if validIds.count >= 3 {
itemId = Int32.max
itemTitle = ChatFolderTitle(text: "+\(component.tags.count - validIds.count)", entities: [], enableAnimations: true)
@@ -404,15 +405,15 @@ private final class ChatListItemTagListComponent: Component {
itemBackgroundColor = itemForegroundColor.withMultipliedAlpha(0.1)
} else {
itemId = tag.id
-
+
let tagColor = PeerNameColor(rawValue: tag.colorId)
let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance)
-
+
itemTitle = ChatFolderTitle(text: tag.title.text.uppercased(), entities: tag.title.entities, enableAnimations: tag.title.enableAnimations)
itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1)
itemForegroundColor = resolvedColor.main
}
-
+
let itemView: ItemView
if let current = self.itemViews[itemId] {
itemView = current
@@ -421,15 +422,15 @@ private final class ChatListItemTagListComponent: Component {
self.itemViews[itemId] = itemView
self.addSubview(itemView)
}
-
+
let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor, sizeFactor: component.sizeFactor)
let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
itemView.frame = itemFrame
itemView.updateVisibility(self.isVisible)
-
+
validIds.append(itemId)
nextX += itemSize.width
-
+
if validIds.count >= 4 {
break
}
@@ -444,15 +445,15 @@ private final class ChatListItemTagListComponent: Component {
for id in removedIds {
self.itemViews.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)
}
@@ -462,20 +463,20 @@ public class ChatListItem: ListViewItem {
public enum EnabledContextActions {
public struct Actions: OptionSet {
public var rawValue: Int32
-
+
public init(rawValue: Int32) {
self.rawValue = rawValue
}
-
+
public static let toggleUnread = Actions(rawValue: 1 << 0)
public static let delete = Actions(rawValue: 1 << 1)
public static let togglePinned = Actions(rawValue: 1 << 2)
}
-
+
case custom(Actions)
case auto
}
-
+
let presentationData: ChatListPresentationData
let context: AccountContext
let chatListLocation: ChatListControllerLocation
@@ -488,15 +489,15 @@ public class ChatListItem: ListViewItem {
let enabledContextActions: EnabledContextActions?
let hiddenOffset: Bool
let interaction: ChatListNodeInteraction
-
+
public let selectable: Bool = true
-
+
public var approximateHeight: CGFloat {
return self.hiddenOffset ? 0.0 : 44.0
}
-
+
let header: ListViewItemHeader?
-
+
public var isPinned: Bool {
switch self.index {
case let .chatList(index):
@@ -509,7 +510,7 @@ public class ChatListItem: ListViewItem {
}
}
}
-
+
public init(presentationData: ChatListPresentationData, context: AccountContext, chatListLocation: ChatListControllerLocation, filterData: ChatListItemFilterData?, index: EngineChatList.Item.Index, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, header: ListViewItemHeader?, enabledContextActions: EnabledContextActions?, hiddenOffset: Bool, interaction: ChatListNodeInteraction) {
self.presentationData = presentationData
self.chatListLocation = chatListLocation
@@ -525,18 +526,18 @@ public class ChatListItem: ListViewItem {
self.hiddenOffset = hiddenOffset
self.interaction = interaction
}
-
+
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListItemNode()
let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
-
+
let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
-
+
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
-
+
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
@@ -548,7 +549,7 @@ public class ChatListItem: ListViewItem {
}
}
}
-
+
public 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) {
Queue.mainQueue().async {
assert(node() is ChatListItemNode)
@@ -561,7 +562,7 @@ public class ChatListItem: ListViewItem {
if case .None = animation {
animated = false
}
-
+
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
@@ -572,7 +573,7 @@ public class ChatListItem: ListViewItem {
}
}
}
-
+
public func selected(listView: ListView) {
switch self.content {
case .loading:
@@ -599,7 +600,7 @@ public class ChatListItem: ListViewItem {
self.interaction.groupSelected(groupReferenceData.groupId)
}
}
-
+
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool) {
var first = false
var last = false
@@ -677,7 +678,7 @@ private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) ->
public struct ChatListItemFilterData: Equatable {
public var excludesArchived: Bool
-
+
public init(excludesArchived: Bool) {
self.excludesArchived = excludesArchived
}
@@ -823,7 +824,7 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
canMarkUnread = false
}
-
+
if canMarkUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, iconColor: theme.list.itemDisclosureActions.accent.foregroundColor, textColor: theme.chatList.dateTextColor))
}
@@ -850,10 +851,10 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation
private final class ChatListItemAccessibilityCustomAction: UIAccessibilityCustomAction {
let key: Int32
-
+
init(name: String, target: Any?, selector: Selector, key: Int32) {
self.key = key
-
+
super.init(name: name, target: target, selector: selector)
}
}
@@ -864,13 +865,13 @@ private final class CachedChatListSearchResult {
let text: String
let searchQuery: String
let resultRanges: [Range]
-
+
init(text: String, searchQuery: String, resultRanges: [Range]) {
self.text = text
self.searchQuery = searchQuery
self.resultRanges = resultRanges
}
-
+
func matches(text: String, searchQuery: String) -> Bool {
if self.text != text {
return false
@@ -885,12 +886,12 @@ private final class CachedChatListSearchResult {
private final class CachedCustomTextEntities {
let text: String
let textEntities: [MessageTextEntity]
-
+
init(text: String, textEntities: [MessageTextEntity]) {
self.text = text
self.textEntities = textEntities
}
-
+
func matches(text: String) -> Bool {
if self.text != text {
return false
@@ -905,39 +906,39 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
private let context: AccountContext
let message: EngineMessage
let media: EngineMedia
-
+
private let imageNode: TransformImageNode
private let playIcon: ASImageNode
-
+
private var requestedImage: Bool = false
private var disposable: Disposable?
-
+
init(context: AccountContext, message: EngineMessage, media: EngineMedia) {
self.context = context
self.message = message
self.media = media
-
+
self.imageNode = TransformImageNode()
self.playIcon = ASImageNode()
self.playIcon.image = playIconImage
-
+
super.init()
-
+
self.addSubnode(self.imageNode)
self.addSubnode(self.playIcon)
}
-
+
deinit {
self.disposable?.dispose()
}
-
+
func updateLayout(size: CGSize, synchronousLoads: Bool) {
if let image = self.playIcon.image {
self.playIcon.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
-
+
let hasSpoiler = self.message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute })
-
+
var isRound = false
var dimensions = CGSize(width: 100.0, height: 100.0)
if case let .image(image) = self.media {
@@ -982,7 +983,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
}
}
}
-
+
let radius: CGFloat
if isRound {
radius = size.width / 2.0
@@ -991,7 +992,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
} else {
radius = 2.0
}
-
+
let makeLayout = self.imageNode.asyncLayout()
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
@@ -1008,7 +1009,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let titleTopicIconView: ComponentHostView?
var titleTopicAvatarNode: AvatarNode?
var titleTopicIconComponent: EmojiStatusComponent?
-
+
var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
@@ -1023,15 +1024,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
private init(topicTitleNode: TextNode, titleTopicIconView: ComponentHostView?, titleTopicAvatarNode: AvatarNode?, titleTopicIconComponent: EmojiStatusComponent?) {
self.topicTitleNode = topicTitleNode
self.titleTopicIconView = titleTopicIconView
self.titleTopicAvatarNode = titleTopicAvatarNode
self.titleTopicIconComponent = titleTopicIconComponent
-
+
super.init()
-
+
self.addSubnode(self.topicTitleNode)
if let titleTopicAvatarNode = self.titleTopicAvatarNode {
self.view.addSubview(titleTopicAvatarNode.view)
@@ -1040,20 +1041,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.view.addSubview(titleTopicIconView)
}
}
-
+
static func asyncLayout(_ currentNode: TopicItemNode?) -> (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ threadPeer: EnginePeer?, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode) {
let makeTopicTitleLayout = TextNode.asyncLayout(currentNode?.topicTitleNode)
-
+
return { constrainedWidth, context, theme, threadId, threadPeer, title, iconId, iconColor in
let remainingWidth = max(1.0, constrainedWidth - (((iconId == nil && iconColor == nil && threadPeer == nil) ? 1.0 : 18.0) + 2.0))
-
+
let topicTitleArguments = TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))
-
+
let topicTitleLayout = makeTopicTitleLayout(topicTitleArguments)
-
+
return (CGSize(width: ((iconId == nil && iconColor == nil && threadPeer == nil) ? 1.0 : 18.0) + 2.0 + topicTitleLayout.0.size.width, height: topicTitleLayout.0.size.height), {
let topicTitleNode = topicTitleLayout.1()
-
+
let titleTopicIconContent: EmojiStatusComponent.Content?
if threadId == 1 {
titleTopicIconContent = .image(image: PresentationResourcesChatList.generalTopicSmallIcon(theme), tintColor: nil)
@@ -1064,10 +1065,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
titleTopicIconContent = nil
}
-
+
var titleTopicIconComponent: EmojiStatusComponent?
var titleTopicIconView: ComponentHostView?
-
+
if let titleTopicIconContent {
titleTopicIconComponent = EmojiStatusComponent(
context: context,
@@ -1077,14 +1078,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
isVisibleForAnimations: (currentNode?.visibilityStatus ?? false) && context.sharedContext.energyUsageSettings.loopEmoji,
action: nil
)
-
+
if let current = currentNode?.titleTopicIconView {
titleTopicIconView = current
} else {
titleTopicIconView = ComponentHostView()
}
}
-
+
var titleTopicAvatarNode: AvatarNode?
if let _ = threadPeer {
if let current = currentNode?.titleTopicAvatarNode {
@@ -1093,11 +1094,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleTopicAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
}
}
-
+
let targetNode = currentNode ?? TopicItemNode(topicTitleNode: topicTitleNode, titleTopicIconView: titleTopicIconView, titleTopicAvatarNode: titleTopicAvatarNode, titleTopicIconComponent: titleTopicIconComponent)
-
+
targetNode.titleTopicIconComponent = titleTopicIconComponent
-
+
if let titleTopicIconView, let titleTopicIconComponent {
let iconSize = titleTopicIconView.update(
transition: .immediate,
@@ -1106,11 +1107,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
containerSize: CGSize(width: 18.0, height: 18.0)
)
titleTopicIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize)
-
+
topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size)
} else if let titleTopicAvatarNode, let threadPeer {
let iconSize = CGSize(width: 18.0, height: 18.0)
-
+
titleTopicAvatarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize)
titleTopicAvatarNode.updateSize(size: iconSize)
if threadPeer.smallProfileImage != nil {
@@ -1118,24 +1119,24 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
titleTopicAvatarNode.setPeer(context: context, theme: theme, peer: threadPeer, overrideImage: nil, emptyColor: theme.list.mediaPlaceholderColor, clipStyle: .round, synchronousLoad: false, displayDimensions: iconSize)
}
-
+
topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size)
} else {
topicTitleNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: topicTitleLayout.0.size)
}
-
+
return targetNode
})
}
}
}
-
+
public final class AuthorNode: ASDisplayNode {
public let authorNode: TextNode
var titleTopicArrowNode: ASImageNode?
var topicNodes: [Int64: TopicItemNode] = [:]
var topicNodeOrder: [Int64] = []
-
+
public var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
@@ -1145,16 +1146,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
override public init() {
self.authorNode = TextNode()
self.authorNode.displaysAsynchronously = true
-
+
super.init()
-
+
self.addSubnode(self.authorNode)
}
-
+
func setFirstTopicHighlighted(_ isHighlighted: Bool) {
guard let id = self.topicNodeOrder.first, let itemNode = self.topicNodes[id] else {
return
@@ -1167,7 +1168,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
itemNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
}
}
-
+
func assignParentNode(parentNode: ASDisplayNode?) {
for (id, topicNode) in self.topicNodes {
if id == self.topicNodeOrder.first, let parentNode {
@@ -1181,24 +1182,24 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topics: [(id: Int64, threadPeer: EnginePeer?, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)]) -> (CGSize, () -> CGRect?) {
let makeAuthorLayout = TextNode.asyncLayout(self.authorNode)
var makeExistingTopicLayouts: [Int64: (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ threadPeer: EnginePeer?, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode)] = [:]
for (topicId, topicNode) in self.topicNodes {
makeExistingTopicLayouts[topicId] = TopicItemNode.asyncLayout(topicNode)
}
-
+
return { [weak self] context, constrainedWidth, theme, authorTitle, topics in
var maxTitleWidth = constrainedWidth
if !topics.isEmpty {
maxTitleWidth = floor(constrainedWidth * 0.7)
}
-
+
let authorTitleLayout = makeAuthorLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
-
+
var remainingWidth = constrainedWidth - authorTitleLayout.0.size.width
-
+
var arrowIconImage: UIImage?
if !topics.isEmpty {
if authorTitle != nil {
@@ -1208,20 +1209,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
var topicsSizeAndApply: [(Int64, CGSize, () -> TopicItemNode)] = []
for topic in topics {
if remainingWidth <= ((topic.iconId == nil && topic.iconColor == nil) ? 8.0 : 22.0) + 2.0 + 10.0 {
break
}
-
+
let makeTopicLayout = makeExistingTopicLayouts[topic.id] ?? TopicItemNode.asyncLayout(nil)
let (topicSize, topicApply) = makeTopicLayout(remainingWidth, context, theme, topic.id, topic.threadPeer, topic.title, topic.iconId, topic.iconColor)
topicsSizeAndApply.append((topic.id, topicSize, topicApply))
-
+
remainingWidth -= topicSize.width + 4.0
}
-
+
var size = authorTitleLayout.0.size
if !topicsSizeAndApply.isEmpty {
for item in topicsSizeAndApply {
@@ -1229,21 +1230,21 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
size.width += 10.0 + item.1.width
}
}
-
+
return (size, {
guard let self else {
return nil
}
-
+
let _ = authorTitleLayout.1()
let authorFrame = CGRect(origin: CGPoint(), size: authorTitleLayout.0.size)
self.authorNode.frame = authorFrame
-
+
var nextX = authorFrame.maxX - 1.0
if authorTitle == nil {
nextX = 0.0
}
-
+
if let arrowIconImage = arrowIconImage {
let titleTopicArrowNode: ASImageNode
if let current = self.titleTopicArrowNode {
@@ -1263,7 +1264,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleTopicArrowNode.removeFromSupernode()
}
}
-
+
var topTopicRect: CGRect?
var topicNodeOrder: [Int64] = []
for item in topicsSizeAndApply {
@@ -1291,33 +1292,33 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.topicNodes.removeValue(forKey: id)
}
self.topicNodeOrder = topicNodeOrder
-
+
return topTopicRect
})
}
}
}
-
+
private struct ContentImageSpec {
var message: EngineMessage
var media: EngineMedia
var size: CGSize
-
+
init(message: EngineMessage, media: EngineMedia, size: CGSize) {
self.message = message
self.media = media
self.size = size
}
}
-
+
public private(set) var item: ChatListItem?
-
+
private let backgroundNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
-
+
let contextContainer: ContextControllerSourceNode
let mainContentContainerNode: ASDisplayNode
-
+
public let avatarContainerNode: ASDisplayNode
public let avatarNode: AvatarNode
var avatarIconView: ComponentHostView?
@@ -1325,9 +1326,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarVideoNode: AvatarVideoNode?
var avatarTapRecognizer: UITapGestureRecognizer?
private var avatarMediaNode: ChatListMediaPreviewNode?
-
+
private var inlineNavigationMarkLayer: SimpleLayer?
-
+
public let titleNode: TextNode
private var titleBadge: (backgroundView: UIImageView, textNode: TextNode)?
public let authorNode: AuthorNode
@@ -1363,42 +1364,44 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var credibilityIconComponent: EmojiStatusComponent?
var statusIconView: ComponentHostView?
var statusIconComponent: EmojiStatusComponent?
+ var winterGramIconView: ComponentHostView?
+ var winterGramIconComponent: EmojiStatusComponent?
let mutedIconNode: ASImageNode
var itemTagList: ComponentView?
var actionButtonTitleNode: TextNode?
var actionButtonBackgroundView: UIImageView?
var actionButtonNode: HighlightableButtonNode?
-
+
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
-
+
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
private var cachedDataDisposable = MetaDisposable()
-
+
private var currentTextLeftCutout: CGFloat = 0.0
private var currentMediaPreviewSpecs: [ContentImageSpec] = []
private var mediaPreviewNodes: [EngineMedia.Id: ChatListMediaPreviewNode] = [:]
-
+
var selectableControlNode: ItemListSelectableControlNode?
var reorderControlNode: ItemListEditableReorderControlNode?
-
+
private var peerPresenceManager: PeerPresenceStatusManager?
-
+
private var cachedChatListText: (String, String)?
private var cachedChatListSearchResult: CachedChatListSearchResult?
private var cachedChatListQuoteSearchResult: CachedChatListSearchResult?
private var cachedCustomTextEntities: CachedCustomTextEntities?
-
+
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool, ListViewItemLayoutParams, countersSize: CGFloat)?
-
+
private var isHighlighted: Bool = false
private var nextHasActiveRevealControls: Bool = false
private var skipFadeout: Bool = false
private var customAnimationInProgress: Bool = false
-
+
private var onlineIsVoiceChat: Bool = false
private var currentOnline: Bool?
-
+
override public var canBeSelected: Bool {
if self.selectableControlNode != nil || self.item?.editing == true {
return false
@@ -1406,7 +1409,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
return super.canBeSelected
}
}
-
+
override public var defaultAccessibilityLabel: String? {
get {
return self.accessibilityLabel
@@ -1425,7 +1428,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} set(value) {
}
}
-
+
override public var accessibilityLabel: String? {
get {
guard let item = self.item else {
@@ -1459,7 +1462,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} set(value) {
}
}
-
+
override public var accessibilityValue: String? {
get {
guard let item = self.item else {
@@ -1529,7 +1532,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} set(value) {
}
}
-
+
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
@@ -1545,7 +1548,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
@@ -1553,9 +1556,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.avatarVideoNode?.resetPlayback()
}
self.updateVideoVisibility()
-
+
self.textNode.visibilityRect = self.visibilityStatus ? CGRect.infinite : nil
-
+
if let verifiedIconView = self.verifiedIconView, let verifiedIconComponent = self.verifiedIconComponent {
let _ = verifiedIconView.update(
transition: .immediate,
@@ -1581,14 +1584,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
)
}
self.authorNode.visibilityStatus = self.visibilityStatus
-
+
if let itemTagListView = self.itemTagList?.view as? ChatListItemTagListComponent.View {
itemTagListView.isVisible = self.visibilityStatus
}
}
}
}
-
+
private var trackingIsInHierarchy: Bool = false {
didSet {
if self.trackingIsInHierarchy != oldValue {
@@ -1601,82 +1604,82 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
-
+
self.avatarContainerNode = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
-
+
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
-
+
self.contextContainer = ContextControllerSourceNode()
-
+
self.mainContentContainerNode = ASDisplayNode()
self.mainContentContainerNode.clipsToBounds = true
-
+
self.measureNode = TextNode()
-
+
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = true
-
+
self.authorNode = AuthorNode()
self.authorNode.isUserInteractionEnabled = false
-
+
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.displaysAsynchronously = true
self.textNode.textNode.anchorPoint = CGPoint()
-
+
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false
self.inputActivitiesNode.alpha = 0.0
-
+
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = true
-
+
self.statusNode = ChatListStatusNode()
self.badgeNode = ChatListBadgeNode()
self.mentionBadgeNode = ChatListBadgeNode()
self.onlineNode = PeerOnlineMarkerNode()
-
+
self.forwardedIconNode = ASImageNode()
self.forwardedIconNode.isLayerBacked = true
self.forwardedIconNode.displaysAsynchronously = false
self.forwardedIconNode.displayWithoutProcessing = true
-
+
self.pinnedIconNode = ASImageNode()
self.pinnedIconNode.isLayerBacked = true
self.pinnedIconNode.displaysAsynchronously = false
self.pinnedIconNode.displayWithoutProcessing = true
-
+
self.mutedIconNode = ASImageNode()
self.mutedIconNode.isLayerBacked = true
self.mutedIconNode.displaysAsynchronously = false
self.mutedIconNode.displayWithoutProcessing = true
-
+
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
-
+
super.init(layerBacked: false, rotated: false, seeThrough: false)
-
+
self.isAccessibilityElement = true
-
+
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
-
+
self.addSubnode(self.contextContainer)
self.contextContainer.addSubnode(self.mainContentContainerNode)
-
+
self.avatarContainerNode.addSubnode(self.avatarNode)
self.contextContainer.addSubnode(self.avatarContainerNode)
self.avatarNode.addSubnode(self.onlineNode)
-
+
self.mainContentContainerNode.addSubnode(self.titleNode)
self.mainContentContainerNode.addSubnode(self.authorNode)
self.mainContentContainerNode.addSubnode(self.textNode.textNode)
@@ -1686,19 +1689,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.mainContentContainerNode.addSubnode(self.badgeNode)
self.mainContentContainerNode.addSubnode(self.mentionBadgeNode)
self.mainContentContainerNode.addSubnode(self.mutedIconNode)
-
+
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.6, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4, layoutParams.5)
let _ = apply(false, false)
}
})
-
+
self.contextContainer.shouldBegin = { [weak self] location in
guard let strongSelf = self, let item = strongSelf.item else {
return false
}
-
+
strongSelf.contextContainer.additionalActivationProgressLayer = nil
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
if case let .peer(peerId) = inlineNavigationLocation.location {
@@ -1713,10 +1716,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
strongSelf.contextContainer.targetNodeForActivationProgress = nil
}
-
+
return true
}
-
+
self.contextContainer.activated = { [weak self] gesture, location in
guard let strongSelf = self, let item = strongSelf.item else {
return
@@ -1729,7 +1732,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
item.interaction.activateChatPreview?(item, threadId, strongSelf.contextContainer, gesture, nil)
}
-
+
self.onDidLoad { [weak self] _ in
guard let self else {
return
@@ -1739,29 +1742,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.avatarNode.view.addGestureRecognizer(avatarTapRecognizer)
}
}
-
+
deinit {
self.cachedDataDisposable.dispose()
}
-
+
override public func secondaryAction(at point: CGPoint) {
guard let item = self.item else {
return
}
item.interaction.activateChatPreview?(item, nil, self.contextContainer, nil, point)
}
-
+
func setupItem(item: ChatListItem, synchronousLoads: Bool) {
let previousItem = self.item
self.item = item
-
+
var storyState: ChatListItemContent.StoryState?
if case let .peer(peerData) = item.content {
storyState = peerData.storyState
} else if case let .groupReference(groupReference) = item.content {
storyState = groupReference.storyState
}
-
+
var peer: EnginePeer?
var displayAsMessage = false
var enablePreview = true
@@ -1790,11 +1793,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}
-
+
if item.interaction.activateChatPreview == nil {
enablePreview = false
}
-
+
self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in
return AvatarNode.StoryStats(
totalCount: storyState.stats.totalCount,
@@ -1808,14 +1811,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
inactiveLineWidth: 1.33
), transition: .immediate)
self.avatarNode.isUserInteractionEnabled = storyState != nil
-
+
if let stats = storyState?.stats, stats.hasLiveItems {
if self.avatarLiveBadge == nil {
let avatarLiveBadge: (outline: UIImageView, foreground: UIImageView) = (UIImageView(), UIImageView())
self.avatarLiveBadge = avatarLiveBadge
self.avatarNode.view.addSubview(avatarLiveBadge.outline)
self.avatarNode.view.addSubview(avatarLiveBadge.foreground)
-
+
let liveString = NSAttributedString(string: item.presentationData.strings.Story_LiveBadge, font: Font.semibold(10.0), textColor: .white)
let liveStringBounds = liveString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let liveBadgeSize = CGSize(width: ceil(liveStringBounds.width) + 4.0 * 2.0, height: ceil(liveStringBounds.height) + 2.0 * 2.0)
@@ -1824,10 +1827,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
defer {
UIGraphicsPopContext()
}
-
+
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0xFF2D55).cgColor)
-
+
func roundedRectCgPath(roundRect rect: CGRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) -> CGPath {
let path = CGMutablePath()
@@ -1871,17 +1874,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
path.closeSubpath()
-
+
return path
}
-
+
let radius = size.height * 0.5
context.addPath(roundedRectCgPath(roundRect: CGRect(origin: CGPoint(), size: size), topLeftRadius: radius, topRightRadius: radius, bottomLeftRadius: radius, bottomRightRadius: radius))
context.fillPath()
-
+
liveString.draw(at: CGPoint(x: floorToScreenPixels((size.width - liveStringBounds.width) * 0.5), y: floorToScreenPixels((size.height - liveStringBounds.height) * 0.5)))
})
-
+
if let image = avatarLiveBadge.foreground.image {
avatarLiveBadge.outline.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
@@ -1893,7 +1896,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
avatarLiveBadge.foreground.removeFromSuperview()
}
}
-
+
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
@@ -1922,13 +1925,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
isForumAvatar = true
}
}
-
+
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
}
-
+
if avatarDiameter != 60.0 {
let avatarFontSize = floor(avatarDiameter * 26.0 / 60.0)
if self.avatarNode.font.pointSize != avatarFontSize {
@@ -1943,14 +1946,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
avatarClipStyle = .round
}
-
+
if peer.smallProfileImage != nil && overrideImage == nil {
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: avatarClipStyle, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter))
} else {
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: avatarClipStyle, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
}
-
- if peer.isPremium && peer.id != item.context.account.peerId {
+
+ if peer.isPremium && !currentWinterGramSettings.hidePremiumStatuses && peer.id != item.context.account.peerId {
let context = item.context
self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView in
@@ -1961,7 +1964,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var personalPhoto: TelegramMediaImage?
var profilePhoto: TelegramMediaImage?
var isKnown = false
-
+
if let cachedPeerData = cachedPeerData {
if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto {
personalPhoto = maybePersonalPhoto
@@ -1976,7 +1979,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
isKnown = true
}
}
-
+
if isKnown {
let photo = personalPhoto ?? profilePhoto
if let photo = photo, item.context.sharedContext.energyUsageSettings.loopEmoji, (!photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil) {
@@ -1989,7 +1992,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarVideoNode = videoNode
}
videoNode.update(peer: peer, photo: photo, size: CGSize(width: 60.0, height: 60.0))
-
+
if strongSelf.hierarchyTrackingLayer == nil {
let hierarchyTrackingLayer = HierarchyTrackingLayer()
hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
@@ -1998,7 +2001,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
strongSelf.trackingIsInHierarchy = true
}
-
+
hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
@@ -2015,7 +2018,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer()
strongSelf.hierarchyTrackingLayer = nil
- }
+ }
strongSelf.updateVideoVisibility()
} else {
if let photo = peer.largeProfileImage, photo.hasVideo {
@@ -2025,18 +2028,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}))
} else {
self.cachedDataDisposable.set(nil)
-
+
self.avatarVideoNode?.removeFromSupernode()
self.avatarVideoNode = nil
-
+
self.hierarchyTrackingLayer?.removeFromSuperlayer()
self.hierarchyTrackingLayer = nil
}
}
-
+
self.contextContainer.isGestureEnabled = enablePreview && !item.editing
}
-
+
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
@@ -2045,19 +2048,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
}
-
+
class func insets(first: Bool, last: Bool, firstWithHeader: Bool) -> UIEdgeInsets {
return UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
}
-
+
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
-
+
self.isHighlighted = highlighted
-
+
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
-
+
var reallyHighlighted: Bool {
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
if let item = self.item {
@@ -2074,12 +2077,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
return reallyHighlighted
}
-
+
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
let highlightProgress: CGFloat = self.item?.interaction.highlightedChatLocation?.progress ?? 1.0
transition.updateCornerRadius(node: self.highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? 26.0 : 0.0)
self.updateSeparatorAlpha(transition: transition)
-
+
if self.reallyHighlighted {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
@@ -2087,11 +2090,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
self.highlightedBackgroundNode.layer.removeAllAnimations()
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
-
+
if let compoundHighlightingNode = self.compoundHighlightingNode {
transition.updateAlpha(layer: compoundHighlightingNode.layer, alpha: 0.0)
}
-
+
if let item = self.item, case .chatList = item.index {
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil, transition: transition)
self.starView?.setOutlineColor(item.presentationData.theme.chatList.itemHighlightedBackgroundColor, transition: transition)
@@ -2106,11 +2109,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
})
}
-
+
if let compoundHighlightingNode = self.compoundHighlightingNode {
transition.updateAlpha(layer: compoundHighlightingNode.layer, alpha: self.authorNode.alpha)
}
-
+
if let item = self.item {
let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
@@ -2125,7 +2128,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition)
}
}
-
+
if let item = self.item {
if let avatarLiveBadge = self.avatarLiveBadge {
let effectiveBackgroundColor: UIColor
@@ -2134,7 +2137,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
-
+
let highlightAlpha = self.highlightedBackgroundNode.supernode == nil ? 0.0 : self.highlightedBackgroundNode.alpha
let outlineColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor.mixedWith(effectiveBackgroundColor, alpha: 1.0 - highlightAlpha)
transition.updateTintColor(view: avatarLiveBadge.outline, color: outlineColor)
@@ -2150,7 +2153,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha)
}
}
-
+
override public func tapped() {
guard let item = self.item, item.editing else {
return
@@ -2166,7 +2169,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool, _ nextHasActiveRevealControls: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
@@ -2182,13 +2185,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let makeActionButtonTitleNodeLayout = TextNode.asyncLayout(self.actionButtonTitleNode)
-
+
let currentItem = self.layoutParams?.0
let currentChatListText = self.cachedChatListText
let currentChatListSearchResult = self.cachedChatListSearchResult
let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult
let currentCustomTextEntities = self.cachedCustomTextEntities
-
+
return { item, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls in
let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0))
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
@@ -2196,7 +2199,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 12.0 / 17.0), design: .regular, weight: .semibold, traits: [.monospacedNumbers])
let avatarBadgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
-
+
let account = item.context.account
var messages: [EngineMessage]
enum ContentPeer {
@@ -2223,9 +2226,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var topForumTopicItems: [EngineChatList.ForumTopicData] = []
var autoremoveTimeout: Int32?
var itemTags: [ChatListItemContent.Tag] = []
-
+
var groupHiddenByDefault = false
-
+
switch item.content {
case .loading:
messages = []
@@ -2261,11 +2264,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let displayAsMessageValue = peerData.displayAsMessage
let forumTopicDataValue = peerData.forumTopicData
let topForumTopicItemsValue = peerData.topForumTopicItems
-
+
itemTags = peerData.tags
-
+
autoremoveTimeout = peerData.autoremoveTimeout
-
+
messages = messagesValue
contentPeer = .chat(peerValue)
combinedReadState = combinedReadStateValue
@@ -2290,7 +2293,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
hasUnseenPollVotes = hasUnseenPollVotesValue
forumTopicData = forumTopicDataValue
topForumTopicItems = topForumTopicItemsValue
-
+
if item.interaction.searchTextHighightState != nil, threadInfo == nil, topForumTopicItems.isEmpty, let message = messagesValue.first, let threadId = message.threadId, let associatedThreadInfo = message.associatedThreadInfo {
var threadPeer: EnginePeer?
if case let .channel(channel) = peerValue.peer, channel.isMonoForum {
@@ -2298,7 +2301,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
topForumTopicItems = [EngineChatList.ForumTopicData(id: threadId, title: associatedThreadInfo.title, iconFileId: associatedThreadInfo.icon, iconColor: associatedThreadInfo.iconColor, maxOutgoingReadMessageId: message.id, isUnread: false, threadPeer: threadPeer)]
}
-
+
switch peerValue.peer {
case .user, .secretChat:
if let peerPresence = peerPresence {
@@ -2315,7 +2318,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
default:
inputActivities = inputActivitiesValue
}
-
+
isPeerGroup = false
promoInfo = promoInfoValue
displayAsMessage = displayAsMessageValue
@@ -2325,7 +2328,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let messageValue = groupReferenceData.message
let unreadCountValue = groupReferenceData.unreadCount
let hiddenByDefault = groupReferenceData.hiddenByDefault
-
+
if let _ = messageValue, !peers.isEmpty {
contentPeer = .chat(peers[0].peer)
} else {
@@ -2352,7 +2355,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
displayAsMessage = false
hasFailedMessages = false
}
-
+
if let messageValue = messages.last {
for media in messageValue.media {
if let media = media as? TelegramMediaAction, case .historyCleared = media.action {
@@ -2360,7 +2363,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
let useChatListLayout: Bool
if case .chatList = item.chatListLocation {
useChatListLayout = true
@@ -2371,15 +2374,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
useChatListLayout = false
}
-
+
let theme = item.presentationData.theme.chatList
-
+
var updatedTheme: PresentationTheme?
-
+
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
-
+
var authorAttributedString: NSAttributedString?
var authorIsCurrentChat: Bool = false
var textAttributedString: NSAttributedString?
@@ -2390,7 +2393,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var badgeContent = ChatListBadgeContent.none
var mentionBadgeContent = ChatListBadgeContent.none
var statusState = ChatListStatusNodeState.none
-
+
var currentBadgeBackgroundImage: UIImage?
var currentAvatarBadgeBackgroundImage: UIImage?
var currentMentionBadgeImage: UIImage?
@@ -2400,13 +2403,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var currentVerifiedIconContent: EmojiStatusComponent.Content?
var currentStatusIconContent: EmojiStatusComponent.Content?
var currentStatusIconParticleColor: UIColor?
+ var currentWinterGramOfficial = false
var currentSecretIconImage: UIImage?
var currentMessageTypeIcon: UIImage?
var currentMessageTypeIconOffset: CGPoint = .zero
-
+
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
-
+
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing {
@@ -2416,15 +2420,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
selectionControlStyle = .compact
}
-
+
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, selectionControlStyle, nil)
if promoInfo == nil && !isPeerGroup {
selectableControlSizeAndApply = sizeAndApply
}
editingOffset = sizeAndApply.0
-
+
var canReorder = false
-
+
if case let .chatList(index) = item.index, index.pinningIndex != nil, promoInfo == nil, !isPeerGroup {
canReorder = true
} else if case let .forum(pinnedIndex, _, _, _, _) = item.index, case .index = pinnedIndex {
@@ -2435,11 +2439,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
canReorder = true
}
-
+
if canReorder {
let sizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = sizeAndApply
@@ -2448,13 +2452,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
editingOffset = 0.0
}
-
+
let enableChatListPhotos = true
-
+
// if changed, adjust setupItem accordingly
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
let avatarLeftInset: CGFloat
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
avatarLeftInset = 17.0 + avatarDiameter
@@ -2467,32 +2471,32 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
avatarLeftInset = 24.0 + avatarDiameter
}
}
-
+
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
let avatarBadgeDiameter: CGFloat = floor(floor(item.presentationData.fontSize.itemListBaseFontSize * 22.0 / 17.0))
let avatarTimerBadgeDiameter: CGFloat = floor(floor(item.presentationData.fontSize.itemListBaseFontSize * 24.0 / 17.0))
-
+
let currentAvatarBadgeCleanBackgroundImage: UIImage? = PresentationResourcesChatList.badgeBackgroundBorder(item.presentationData.theme, diameter: avatarBadgeDiameter + 4.0)
-
+
let leftInset: CGFloat = params.leftInset + avatarLeftInset
-
+
enum ContentData {
case chat(itemPeer: EngineRenderedPeer, threadInfo: ChatListItemContent.ThreadInfo?, peer: EnginePeer?, hideAuthor: Bool, messageText: String, messageEntities: [MessageTextEntity], spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?)
case group(peers: [EngineChatList.GroupItem.Item])
}
-
+
let contentData: ContentData
-
+
var hideAuthor = false
switch contentPeer {
case let .chat(itemPeer):
var (peer, initialHideAuthor, messageText, messageEntities, spoilers, customEmojiRanges) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
-
+
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
initialHideAuthor = true
messageText = psaText
}
-
+
switch itemPeer.peer {
case .user:
if let attribute = messages.first?._asMessage().reactionsAttribute {
@@ -2513,17 +2517,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
default:
break
}
-
+
contentData = .chat(itemPeer: itemPeer, threadInfo: threadInfo, peer: peer, hideAuthor: hideAuthor, messageText: messageText, messageEntities: messageEntities, spoilers: spoilers, customEmojiRanges: customEmojiRanges)
hideAuthor = initialHideAuthor
case let .group(groupPeers):
contentData = .group(peers: groupPeers)
hideAuthor = true
}
-
+
var attributedText: NSAttributedString
var hasDraft = false
-
+
var inlineAuthorPrefix: String?
var useInlineAuthorPrefix = false
if case .groupReference = item.content {
@@ -2532,7 +2536,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if !itemTags.isEmpty {
forumTopicData = nil
topForumTopicItems = []
-
+
if case let .chat(itemPeer, _, _, _, _, _, _, _) = contentData {
if let messagePeer = itemPeer.chatMainPeer {
switch messagePeer {
@@ -2548,7 +2552,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
if useInlineAuthorPrefix {
if case let .user(author) = messages.last?.author {
if author.id == item.context.account.peerId {
@@ -2558,18 +2562,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
var chatListText: (String, String)?
var chatListSearchResult: CachedChatListSearchResult?
var chatListQuoteSearchResult: CachedChatListSearchResult?
var customTextEntities: CachedCustomTextEntities?
-
+
let contentImageSide: CGFloat = max(10.0, min(20.0, floor(item.presentationData.fontSize.baseDisplaySize * 18.0 / 17.0)))
let contentImageSize = CGSize(width: contentImageSide, height: contentImageSide)
let contentImageSpacing: CGFloat = 2.0
let forwardedIconSpacing: CGFloat = 6.0
let contentImageTrailingSpace: CGFloat = 5.0
-
+
var contentImageSpecs: [ContentImageSpec] = []
var avatarContentImageSpec: ContentImageSpec?
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, threadPeer: EnginePeer?, isUnread: Bool)?
@@ -2595,7 +2599,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
var messageTypeIcon: MessageTypeIcon?
var ignoreForwardedIcon = false
-
+
switch contentData {
case let .chat(itemPeer, _, _, _, text, entities, spoilers, customEmojiRanges):
var isUser = false
@@ -2635,7 +2639,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
if case .chatList = item.chatListLocation, itemPeer.peerId == item.context.account.peerId, let message = messages.first {
var effectiveAuthor: EngineRawPeer? = message.author?._asPeer()
if let forwardInfo = message.forwardInfo {
@@ -2651,14 +2655,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
effectiveAuthor = TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.Empty, id: EnginePeer.Id.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}
-
+
if let effectiveAuthor, effectiveAuthor.id != itemPeer.chatMainPeer?.id {
authorIsCurrentChat = false
peerText = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
ignoreForwardedIcon = true
}
}
-
+
if let _ = peerText, case let .channel(channel) = itemPeer.chatMainPeer, channel.isForumOrMonoForum, threadInfo == nil {
if let forumTopicData {
forumThread = (forumTopicData.id, forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor, forumTopicData.threadPeer, forumTopicData.isUnread)
@@ -2669,7 +2673,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let forumTopicData, forumThread == nil, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) {
forumThread = (forumTopicData.id, forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor, forumTopicData.threadPeer, forumTopicData.isUnread)
}
-
+
let messageText: String
if let currentChatListText = currentChatListText, currentChatListText.0 == text {
messageText = currentChatListText.1
@@ -2684,11 +2688,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
chatListText = (text, messageText)
}
-
+
if inlineAuthorPrefix == nil, let mediaDraftContentType {
hasDraft = true
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
-
+
switch mediaDraftContentType {
case .audio:
attributedText = NSAttributedString(string: item.presentationData.strings.Message_Audio, font: textFont, textColor: theme.messageTextColor)
@@ -2698,7 +2702,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if inlineAuthorPrefix == nil, let draftState = draftState {
hasDraft = true
let draftText = stringWithAppliedEntities(draftState.text, entities: draftState.entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil)
-
+
if !itemTags.isEmpty {
let tempAttributedText = foldLineBreaks(draftText)
let attributedTextWithDraft = NSMutableAttributedString()
@@ -2707,16 +2711,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
attributedText = attributedTextWithDraft
} else {
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
-
+
attributedText = foldLineBreaks(draftText)
}
} else if let message = messages.last {
var composedString: NSMutableAttributedString
-
+
if let peerText = peerText {
authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor)
}
-
+
var entities = entities.filter { entity in
switch entity.type {
case .Spoiler, .CustomEmoji, .FormattedDate:
@@ -2730,7 +2734,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) {
entities = []
}
-
+
if message.id.peerId.isTelegramNotifications || message.id.peerId.isVerificationCodes {
let regex: NSRegularExpression?
if message.id.peerId.isTelegramNotifications {
@@ -2748,11 +2752,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
customTextEntities = CachedCustomTextEntities(text: messageText, textEntities: entities)
}
}
-
+
if let customTextEntities, !customTextEntities.textEntities.isEmpty {
entities.append(contentsOf: customTextEntities.textEntities)
}
-
+
let messageString: NSAttributedString
if !messageText.isEmpty && entities.count > 0 {
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())
@@ -2792,7 +2796,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
composedString = NSMutableAttributedString(attributedString: messageString)
}
-
+
var composedReplyString: NSMutableAttributedString?
if let searchQuery = item.interaction.searchTextHighightState {
var quoteText: String?
@@ -2811,14 +2815,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let quoteString = foldLineBreaks(stringWithAppliedEntities(quoteText, entities: [], baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil))
composedReplyString = NSMutableAttributedString(attributedString: quoteString)
}
-
+
if let cached = currentChatListSearchResult, cached.matches(text: composedString.string, searchQuery: searchQuery) {
chatListSearchResult = cached
} else {
let (ranges, text) = findSubstringRanges(in: composedString.string, query: searchQuery)
chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges)
}
-
+
if let composedReplyString {
if let cached = currentChatListQuoteSearchResult, cached.matches(text: composedReplyString.string, searchQuery: searchQuery) {
chatListQuoteSearchResult = cached
@@ -2833,7 +2837,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
chatListSearchResult = nil
chatListQuoteSearchResult = nil
}
-
+
if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first {
for range in chatListSearchResult.resultRanges {
let stringRange = NSRange(range, in: chatListSearchResult.text)
@@ -2847,7 +2851,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
composedString.addAttribute(.foregroundColor, value: theme.messageHighlightedTextColor, range: stringRange)
}
}
-
+
let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound)
if firstRangeOrigin > 24 && !chatListSearchResult.searchQuery.hasPrefix("#") {
var leftOrigin: Int = 0
@@ -2873,7 +2877,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
composedReplyString.addAttribute(.foregroundColor, value: theme.messageHighlightedTextColor, range: stringRange)
}
}
-
+
let firstRangeOrigin = chatListQuoteSearchResult.text.distance(from: chatListQuoteSearchResult.text.startIndex, to: firstRange.lowerBound)
if firstRangeOrigin > 24 {
var leftOrigin: Int = 0
@@ -2886,12 +2890,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
composedReplyString = composedReplyString.attributedSubstring(from: NSMakeRange(leftOrigin, composedReplyString.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString
composedReplyString.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: theme.messageTextColor]), at: 0)
}
-
+
composedString = composedReplyString
}
-
+
attributedText = composedString
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let commandPrefix = customMessageListData.commandPrefix {
let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText)
let boldTextFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
@@ -2904,7 +2908,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
attributedText = mutableAttributedText
}
-
+
if !ignoreForwardedIcon {
if case .savedMessagesChats = item.chatListLocation {
} else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) && !message.id.peerId.isVerificationCodes {
@@ -2943,7 +2947,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
var displayMediaPreviews = true
if message._asMessage().containsSecretMedia {
displayMediaPreviews = false
@@ -2953,17 +2957,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if displayMediaPreviews {
let contentImageFillSize = CGSize(width: 8.0, height: contentImageSize.height)
_ = contentImageFillSize
-
+
var contentImageIsDisplayedAsAvatar = false
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
contentImageIsDisplayedAsAvatar = true
}
-
+
for message in messages {
if contentImageSpecs.count >= 3 {
break
}
-
+
inner: for media in message.media {
if let paidContent = media as? TelegramMediaPaidContent {
let fitSize = contentImageSize
@@ -3047,7 +3051,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
if contentImageIsDisplayedAsAvatar {
avatarContentImageSpec = contentImageSpecs.first
contentImageSpecs.removeAll()
@@ -3055,14 +3059,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
} else {
attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
-
+
var peerText: String?
if case .groupReference = item.content {
if let messagePeer = itemPeer.chatMainPeer {
peerText = messagePeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
}
-
+
if let peerText = peerText {
authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor)
}
@@ -3089,7 +3093,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
attributedText = textString
}
-
+
switch messageTypeIcon {
case let .call(type, direction):
switch type {
@@ -3135,7 +3139,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
break
}
let messageTypeIconScale = min(1.0, item.presentationData.fontSize.itemListBaseFontSize / 17.0)
-
+
if let currentMessageTypeIcon {
textLeftCutout += currentMessageTypeIcon.size.width * messageTypeIconScale
if !contentImageSpecs.isEmpty {
@@ -3144,7 +3148,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textLeftCutout += contentImageTrailingSpace - 1.0
}
}
-
+
for i in 0 ..< contentImageSpecs.count {
if i != 0 {
textLeftCutout += contentImageSpacing
@@ -3154,7 +3158,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textLeftCutout += contentImageTrailingSpace
}
}
-
+
switch contentData {
case let .chat(itemPeer, threadInfo, _, _, _, _, _, _):
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
@@ -3169,7 +3173,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textColor = theme.titleColor
}
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
-
+
if case let .channel(channel) = itemPeer.peer, channel.flags.contains(.isMonoforum) {
titleBadgeText = item.presentationData.strings.ChatList_MonoforumLabel
}
@@ -3206,9 +3210,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
case .group:
titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_ArchivedChatsTitle, font: titleFont, textColor: theme.titleColor)
}
-
+
textAttributedString = attributedText
-
+
let dateText: String
var topIndex: EngineMessage.Index?
switch item.content {
@@ -3227,14 +3231,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var t = Int(topIndex.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
-
+
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
-
+
dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: topIndex.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
} else {
dateText = ""
}
-
+
if isPeerGroup {
dateAttributedString = NSAttributedString(string: "", font: dateFont, textColor: theme.dateTextColor)
} else if let promoInfo = promoInfo {
@@ -3254,7 +3258,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor)
}
-
+
if !isPeerGroup, let message = messages.last, message.author?.id == account.peerId && !hasDraft {
if message.flags.isSending && !message._asMessage().isSentOrAcknowledged {
statusState = .clock(PresentationResourcesChatList.clockFrameImage(item.presentationData.theme), PresentationResourcesChatList.clockMinImage(item.presentationData.theme))
@@ -3278,7 +3282,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
if unreadCount.unread {
let badgeTextColor: UIColor
if unreadCount.muted {
@@ -3310,7 +3314,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} 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)
@@ -3344,28 +3348,28 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
currentPinnedIconImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme, diameter: badgeDiameter)
}
}
-
+
let isMuted = isRemovedFromTotalUnreadCount
if isMuted {
currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.presentationData.theme)
}
-
+
var statusWidth: CGFloat
if case .none = statusState {
statusWidth = 0.0
} else {
statusWidth = 24.0
}
-
+
var dateIconImage: UIImage?
if let threadInfo, threadInfo.isClosed {
dateIconImage = PresentationResourcesChatList.statusLockIcon(item.presentationData.theme)
}
-
+
if let dateIconImage {
statusWidth += dateIconImage.size.width + 4.0
}
-
+
var titleIconsWidth: CGFloat = 0.0
if let currentMutedIconImage = currentMutedIconImage {
if titleIconsWidth.isZero {
@@ -3373,7 +3377,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
titleIconsWidth += currentMutedIconImage.size.width
}
-
+
var isSubscription = false
var isSecret = false
if !isPeerGroup {
@@ -3384,13 +3388,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if isSecret {
currentSecretIconImage = PresentationResourcesChatList.secretIcon(item.presentationData.theme)
}
-
+
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
var isAccountPeer = false
if case let .chatList(index) = item.index, index.messageIndex.id.peerId == item.context.account.peerId {
isAccountPeer = true
}
-
+
if !isPeerGroup && !isAccountPeer && threadInfo == nil {
if displayAsMessage {
switch item.content {
@@ -3401,7 +3405,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
iconPeer = peerData.messages.last?.author
}
-
+
if let peer = iconPeer {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
currentCredibilityIconContent = nil
@@ -3416,16 +3420,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let color = emojiStatus.color {
currentStatusIconParticleColor = UIColor(rgb: UInt32(bitPattern: color))
}
- } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
+ } else if peer.isPremium && !currentWinterGramSettings.hidePremiumStatuses && !premiumConfiguration.isPremiumDisabled {
currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor)
}
-
+
if peer.isVerified {
currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
}
if let verificationIconFileId = peer.verificationIconFileId {
currentVerifiedIconContent = .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))
}
+ currentWinterGramOfficial = isWinterGramOfficialPeer(peer)
}
default:
break
@@ -3447,28 +3452,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let color = emojiStatus.color {
currentStatusIconParticleColor = UIColor(rgb: UInt32(bitPattern: color))
}
- } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
+ } else if peer.isPremium && !currentWinterGramSettings.hidePremiumStatuses && !premiumConfiguration.isPremiumDisabled {
currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor)
}
-
+
if peer.isVerified {
currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
}
if let verificationIconFileId = peer.verificationIconFileId {
currentVerifiedIconContent = .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))
}
+ currentWinterGramOfficial = isWinterGramOfficialPeer(peer)
}
}
if let currentSecretIconImage = currentSecretIconImage {
titleIconsWidth += currentSecretIconImage.size.width + 2.0
}
-
+
var titleLeftOffset: CGFloat = 0.0
if let currentVerifiedIconContent {
if titleLeftOffset.isZero, case .animation = currentVerifiedIconContent {
titleLeftOffset += 19.0
}
-
+
if titleIconsWidth.isZero {
titleIconsWidth += 4.0
} else {
@@ -3483,7 +3489,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleIconsWidth += 8.0
}
}
-
+
if let currentCredibilityIconContent {
if titleIconsWidth.isZero {
titleIconsWidth += 4.0
@@ -3499,7 +3505,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleIconsWidth += 8.0
}
}
-
+
if let currentStatusIconContent {
if titleIconsWidth.isZero {
titleIconsWidth += 4.0
@@ -3515,22 +3521,31 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleIconsWidth += 8.0
}
}
-
+
+ if currentWinterGramOfficial {
+ if titleIconsWidth.isZero {
+ titleIconsWidth += 4.0
+ } else {
+ titleIconsWidth += 2.0
+ }
+ titleIconsWidth += 18.0
+ }
+
let layoutOffset: CGFloat = 0.0
-
+
let rawContentWidth = params.width - leftInset - params.rightInset - 18.0 - editingOffset
-
+
let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentBadgeBackgroundImage, badgeContent)
-
+
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
-
+
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
-
+
var badgeSize: CGFloat = 0.0
if !badgeLayout.width.isZero {
badgeSize += badgeLayout.width + 5.0
@@ -3560,15 +3575,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
badgeSize += actionButtonTitleNodeLayout.size.width + 12.0 * 2.0
}
badgeSize = max(badgeSize, reorderInset)
-
+
if !itemTags.isEmpty {
authorAttributedString = nil
}
-
+
var effectiveAuthorTitle = (hideAuthor && !hasDraft) ? nil : authorAttributedString
-
+
let isSearching = item.interaction.searchTextHighightState != nil
-
+
var isFirstForumThreadSelectable = false
var forumThreads: [(id: Int64, threadPeer: EnginePeer?, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)] = []
if case .savedMessagesChats = item.chatListLocation {
@@ -3583,15 +3598,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
forumThreads.append((id: topicItem.id, threadPeer: topicItem.threadPeer, title: NSAttributedString(string: topicItem.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil))
}
}
-
+
if let effectiveAuthorTitle, let textAttributedStringValue = textAttributedString {
let mutableTextAttributedString = NSMutableAttributedString()
mutableTextAttributedString.append(NSAttributedString(string: effectiveAuthorTitle.string + ": ", font: textFont, textColor: theme.authorNameColor))
mutableTextAttributedString.append(textAttributedStringValue)
-
+
textAttributedString = mutableTextAttributedString
}
-
+
effectiveAuthorTitle = nil
}
} else if forumThread != nil || !topForumTopicItems.isEmpty {
@@ -3601,36 +3616,36 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
isFirstForumThreadSelectable = forumThread.isUnread
}
-
+
forumThreads.append((id: forumThread.id, threadPeer: forumThread.threadPeer, title: NSAttributedString(string: forumThread.title, font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: forumThread.iconId, iconColor: forumThread.iconColor))
}
for topicItem in topForumTopicItems {
if case let .peer(peer) = item.content, peer.peer.peerId.id._internalGetInt64Value() == topicItem.id {
-
+
} else if forumThread?.id != topicItem.id {
forumThreads.append((id: topicItem.id, threadPeer: topicItem.threadPeer, title: NSAttributedString(string: topicItem.title, font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: topicItem.iconFileId, iconColor: topicItem.iconColor))
}
}
-
+
if let effectiveAuthorTitle, let textAttributedStringValue = textAttributedString {
let mutableTextAttributedString = NSMutableAttributedString()
mutableTextAttributedString.append(NSAttributedString(string: effectiveAuthorTitle.string + ": ", font: textFont, textColor: theme.authorNameColor))
mutableTextAttributedString.append(textAttributedStringValue)
-
+
textAttributedString = mutableTextAttributedString
}
-
+
effectiveAuthorTitle = nil
}
-
+
if authorIsCurrentChat {
effectiveAuthorTitle = nil
}
-
+
let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads)
-
+
var textBottomRightCutout: CGFloat = 0.0
-
+
let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0)
var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 {
@@ -3641,20 +3656,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
trailingTextBadgeLayoutAndApply = (layout, apply)
textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right
}
-
+
var textCutout: TextNodeCutout?
if !textLeftCutout.isZero || !textBottomRightCutout.isZero {
textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0))
}
-
+
var textMaxWidth = rawContentWidth - badgeSize
-
+
var textArrowImage: UIImage?
if isFirstForumThreadSelectable {
textArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
textMaxWidth -= 18.0
}
-
+
let textLineSpacing: CGFloat = min(0.2, item.presentationData.fontSize.itemListBaseFontSize * 0.2 / 17.0)
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(
attributedString: textAttributedString,
@@ -3667,7 +3682,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
cutout: textCutout,
insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)
))
-
+
let maxTitleLines: Int
switch item.index {
case .forum:
@@ -3675,31 +3690,31 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
case .chatList:
maxTitleLines = 1
}
-
+
var titleLeftCutout: CGFloat = 0.0
if item.interaction.isInlineMode {
titleLeftCutout = 22.0
}
-
+
if let titleAttributedStringValue = titleAttributedString, titleAttributedStringValue.length == 0 {
titleAttributedString = NSAttributedString(string: " ", font: titleFont, textColor: theme.titleColor)
}
-
+
var titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth
var titleCutout: TextNodeCutout?
if !titleLeftCutout.isZero {
titleCutout = TextNodeCutout(topLeft: CGSize(width: titleLeftCutout, height: 10.0), topRight: nil, bottomRight: nil)
}
-
+
var titleBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let titleBadgeText {
let titleBadgeLayoutAndApplyValue = titleBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleBadgeText, font: Font.semibold(11.0), textColor: theme.titleColor.withMultipliedAlpha(0.4)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
titleBadgeLayoutAndApply = titleBadgeLayoutAndApplyValue
titleRectWidth = max(10.0, titleRectWidth - titleBadgeLayoutAndApplyValue.0.size.width - 8.0)
}
-
+
let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: maxTitleLines, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: titleCutout, insets: UIEdgeInsets()))
-
+
var inputActivitiesSize: CGSize?
var inputActivitiesApply: (() -> Void)?
var chatPeerId: EnginePeer.Id?
@@ -3717,11 +3732,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
inputActivitiesSize = size
inputActivitiesApply = apply
}
-
+
var online = false
var animateOnline = false
var onlineIsVoiceChat = false
-
+
var isPinned = false
if case let .chatList(index) = item.index {
isPinned = index.pinningIndex != nil
@@ -3741,7 +3756,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let renderedPeer = peerData.peer
let presence = peerData.presence
let displayAsMessage = peerData.displayAsMessage
-
+
if !displayAsMessage {
if case let .user(peer) = renderedPeer.chatMainPeer, let presence = presence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
let updatedPresence = EnginePeer.Presence(status: presence.status, lastActivity: 0)
@@ -3765,7 +3780,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
animateOnline = true
}
}
-
+
if let enabledContextActions = item.enabledContextActions {
switch enabledContextActions {
case .auto:
@@ -3823,7 +3838,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
case let .custom(actions):
peerRevealOptions = []
peerLeftRevealOptions = []
-
+
if actions.contains(.toggleUnread) {
if unreadCount.unread {
peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Read, icon: readIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor))
@@ -3843,20 +3858,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
peerRevealOptions = groupReferenceRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isEditing: item.editing, hiddenByDefault: groupHiddenByDefault)
peerLeftRevealOptions = []
}
-
+
if item.interaction.inlineNavigationLocation != nil {
peerRevealOptions = []
peerLeftRevealOptions = []
}
-
+
let (onlineLayout, onlineApply) = onlineLayout(online, onlineIsVoiceChat)
var animateContent = false
if let currentItem = currentItem, currentItem.content.chatLocation == item.content.chatLocation {
animateContent = true
}
-
+
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: " ", font: titleFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
let titleSpacing: CGFloat = -1.0
let authorSpacing: CGFloat = -3.0
var itemHeight: CGFloat = 8.0 * 2.0 + 1.0
@@ -3870,16 +3885,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
itemHeight += titleSpacing
itemHeight += authorSpacing
}
-
+
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
-
+
let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
var heightOffset: CGFloat = 0.0
if item.hiddenOffset {
heightOffset = -itemHeight
}
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: max(0.0, itemHeight + heightOffset)), insets: insets)
-
+
var customActions: [ChatListItemAccessibilityCustomAction] = []
for option in peerLeftRevealOptions {
customActions.append(ChatListItemAccessibilityCustomAction(name: option.title, target: nil, selector: #selector(ChatListItemNode.performLocalAccessibilityCustomAction(_:)), key: option.key))
@@ -3887,7 +3902,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
for option in peerRevealOptions {
customActions.append(ChatListItemAccessibilityCustomAction(name: option.title, target: nil, selector: #selector(ChatListItemNode.performLocalAccessibilityCustomAction(_:)), key: option.key))
}
-
+
return (layout, { [weak self] synchronousLoads, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls, params, countersSize)
@@ -3898,50 +3913,50 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.cachedChatListQuoteSearchResult = chatListQuoteSearchResult
strongSelf.cachedCustomTextEntities = customTextEntities
strongSelf.onlineIsVoiceChat = onlineIsVoiceChat
-
+
var animateOnline = animateOnline
if let currentOnline = strongSelf.currentOnline, currentOnline == online {
animateOnline = false
}
strongSelf.currentOnline = online
-
+
if item.hiddenOffset {
strongSelf.layer.zPosition = -1.0
}
-
+
if case .groupReference = item.content {
strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, layout.contentSize.height - itemHeight, 0.0)
}
-
+
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.chatList.itemSeparatorColor
}
-
+
let revealOffset = 0.0
-
+
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
-
+
transition.updateAlpha(node: strongSelf, alpha: item.hiddenOffset ? 0.0 : 1.0)
ComponentTransition(transition).setBlur(layer: strongSelf.layer, radius: item.hiddenOffset ? 8.0 : 0.0)
-
+
let contextContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: itemHeight))
// strongSelf.contextContainer.position = contextContainerFrame.center
transition.updatePosition(node: strongSelf.contextContainer, position: contextContainerFrame.center)
transition.updateBounds(node: strongSelf.contextContainer, bounds: contextContainerFrame.offsetBy(dx: -strongSelf.revealOffset, dy: 0.0))
-
+
var mainContentFrame: CGRect
var mainContentBoundsOffset: CGFloat
var mainContentAlpha: CGFloat = 1.0
-
+
if useChatListLayout {
mainContentFrame = CGRect(origin: CGPoint(x: leftInset - 2.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height))
mainContentBoundsOffset = mainContentFrame.origin.x
-
+
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
mainContentAlpha = 1.0 - inlineNavigationLocation.progress
mainContentBoundsOffset += (mainContentFrame.width - mainContentFrame.minX) * inlineNavigationLocation.progress
@@ -3950,12 +3965,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
mainContentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height))
mainContentBoundsOffset = 0.0
}
-
+
transition.updatePosition(node: strongSelf.mainContentContainerNode, position: mainContentFrame.center)
-
+
transition.updateBounds(node: strongSelf.mainContentContainerNode, bounds: CGRect(origin: CGPoint(x: mainContentBoundsOffset, y: 0.0), size: mainContentFrame.size))
transition.updateAlpha(node: strongSelf.mainContentContainerNode, alpha: mainContentAlpha)
-
+
var crossfadeContent = false
if let selectableControlSizeAndApply = selectableControlSizeAndApply {
let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height)
@@ -3983,7 +3998,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
selectableControlNode?.removeFromSupernode()
})
}
-
+
var animateBadges = animateContent
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: layoutOffset), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
@@ -3994,7 +4009,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
reorderControlNode.frame = reorderControlFrame
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
-
+
transition.updateAlpha(node: strongSelf.dateNode, alpha: 0.0)
if let dateStatusIconNode = strongSelf.dateStatusIconNode {
transition.updateAlpha(node: dateStatusIconNode, alpha: 0.0)
@@ -4022,32 +4037,32 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 1.0)
}
-
+
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
-
+
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + 6.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
var avatarScaleOffset: CGFloat = 0.0
var avatarScale: CGFloat = 1.0
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
let targetAvatarScale: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 54.0 / 17.0) / avatarFrame.width
avatarScale = targetAvatarScale * inlineNavigationLocation.progress + 1.0 * (1.0 - inlineNavigationLocation.progress)
-
+
let targetAvatarScaleOffset: CGFloat = -(avatarFrame.width - avatarFrame.width * avatarScale) * 0.5
avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress
}
-
+
transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame)
transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0))
transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale)
strongSelf.avatarNode.updateSize(size: avatarFrame.size)
strongSelf.updateVideoVisibility()
-
+
var itemPeerId: EnginePeer.Id?
if case let .chatList(index) = item.index {
itemPeerId = index.messageIndex.id.peerId
}
-
+
if let itemPeerId = itemPeerId, let inlineNavigationLocation = item.interaction.inlineNavigationLocation, inlineNavigationLocation.location.peerId == itemPeerId {
let inlineNavigationMarkLayer: SimpleLayer
var animateIn = false
@@ -4076,10 +4091,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updatePosition(layer: inlineNavigationMarkLayer, position: CGPoint(x: -inlineNavigationMarkLayer.bounds.width * 0.5, y: avatarFrame.midY))
}
}
-
+
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation, badgeContent != .none {
var animateIn = false
-
+
let avatarBadgeBackground: ASImageNode
if let current = strongSelf.avatarBadgeBackground {
avatarBadgeBackground = current
@@ -4088,9 +4103,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarBadgeBackground = avatarBadgeBackground
strongSelf.avatarNode.addSubnode(avatarBadgeBackground)
}
-
+
avatarBadgeBackground.image = currentAvatarBadgeCleanBackgroundImage
-
+
let avatarBadgeNode: ChatListBadgeNode
if let current = strongSelf.avatarBadgeNode {
avatarBadgeNode = current
@@ -4101,18 +4116,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarBadgeNode = avatarBadgeNode
strongSelf.avatarNode.addSubnode(avatarBadgeNode)
}
-
+
let makeAvatarBadgeLayout = avatarBadgeNode.asyncLayout()
let (avatarBadgeLayout, avatarBadgeApply) = makeAvatarBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), avatarBadgeDiameter, avatarBadgeFont, currentAvatarBadgeBackgroundImage, badgeContent)
let _ = avatarBadgeApply(animateBadges, false)
let avatarBadgeFrame = CGRect(origin: CGPoint(x: avatarFrame.width - avatarBadgeLayout.width, y: avatarFrame.height - avatarBadgeLayout.height), size: avatarBadgeLayout)
avatarBadgeNode.position = avatarBadgeFrame.center
avatarBadgeNode.bounds = CGRect(origin: CGPoint(), size: avatarBadgeFrame.size)
-
+
let avatarBadgeBackgroundFrame = avatarBadgeFrame.insetBy(dx: -2.0, dy: -2.0)
avatarBadgeBackground.position = avatarBadgeBackgroundFrame.center
avatarBadgeBackground.bounds = CGRect(origin: CGPoint(), size: avatarBadgeBackgroundFrame.size)
-
+
if animateIn {
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.00001)
ContainedViewLayoutTransition.immediate.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.00001)
@@ -4131,7 +4146,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
})
}
}
-
+
if let threadInfo = threadInfo, !displayAsMessage {
let avatarIconView: ComponentHostView
if let current = strongSelf.avatarIconView {
@@ -4141,7 +4156,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarIconView = avatarIconView
strongSelf.mainContentContainerNode.view.addSubview(avatarIconView)
}
-
+
let avatarIconContent: EmojiStatusComponent.Content
if threadInfo.id == 1 {
avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(item.presentationData.theme), tintColor: nil)
@@ -4150,7 +4165,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
avatarIconContent = .topic(title: String(threadInfo.info.title.prefix(1)), color: threadInfo.info.iconColor, size: CGSize(width: 32.0, height: 32.0))
}
-
+
let avatarIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: item.interaction.animationCache,
@@ -4160,14 +4175,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
action: nil
)
strongSelf.avatarIconComponent = avatarIconComponent
-
+
let iconSize = avatarIconView.update(
transition: .immediate,
component: AnyComponent(avatarIconComponent),
environment: {},
containerSize: item.interaction.isInlineMode ? CGSize(width: 18.0, height: 18.0) : CGSize(width: 32.0, height: 32.0)
)
-
+
let avatarIconFrame: CGRect
if item.interaction.isInlineMode {
avatarIconFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 1.0), size: iconSize)
@@ -4179,13 +4194,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarIconView = nil
avatarIconView.removeFromSuperview()
}
-
+
if !useChatListLayout {
strongSelf.avatarContainerNode.isHidden = true
} else {
strongSelf.avatarContainerNode.isHidden = false
}
-
+
let onlineFrame: CGRect
if onlineIsVoiceChat {
onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.width - onlineLayout.width + 1.0 - UIScreenPixel, y: avatarFrame.height - onlineLayout.height + 1.0 - UIScreenPixel), size: onlineLayout)
@@ -4193,29 +4208,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.width - onlineLayout.width - 2.0, y: avatarFrame.height - onlineLayout.height - 2.0), size: onlineLayout)
}
transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame)
-
+
if let avatarLiveBadge = strongSelf.avatarLiveBadge, let iconImage = avatarLiveBadge.foreground.image, let outlineImage = avatarLiveBadge.outline.image {
let outlineInset = (outlineImage.size.height - iconImage.size.height) * 0.5
let liveBadgeFrame = CGRect(origin: CGPoint(x: floor((avatarFrame.width - iconImage.size.width) * 0.5), y: avatarFrame.height + 5.0 - iconImage.size.height), size: iconImage.size)
transition.updateFrame(view: avatarLiveBadge.foreground, frame: liveBadgeFrame)
transition.updateFrame(view: avatarLiveBadge.outline, frame: liveBadgeFrame.insetBy(dx: -outlineInset, dy: -outlineInset))
-
+
let effectiveBackgroundColor: UIColor
if item.isPinned {
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else {
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
-
+
let highlightAlpha = strongSelf.highlightedBackgroundNode.supernode == nil ? 0.0 : strongSelf.highlightedBackgroundNode.alpha
let outlineColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor.mixedWith(effectiveBackgroundColor, alpha: 1.0 - highlightAlpha)
transition.updateTintColor(view: avatarLiveBadge.outline, color: outlineColor)
}
-
+
let onlineInlineNavigationFraction: CGFloat = item.interaction.inlineNavigationLocation?.progress ?? 0.0
transition.updateAlpha(node: strongSelf.onlineNode, alpha: 1.0 - onlineInlineNavigationFraction)
transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001)
-
+
let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
if strongSelf.reallyHighlighted {
@@ -4229,7 +4244,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
strongSelf.onlineNode.setImage(onlineIcon, color: item.presentationData.theme.list.itemCheckColors.foregroundColor, transition: .immediate)
-
+
if isSubscription, autoremoveTimeout == nil {
let starView: StarView
if let current = strongSelf.starView {
@@ -4240,7 +4255,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.contextContainer.view.addSubview(starView)
}
starView.outlineColor = effectiveBackgroundColor
-
+
let starSize = CGSize(width: 20.0, height: 20.0)
let starFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - starSize.width + 1.0, y: avatarFrame.maxY - starSize.height + 1.0), size: starSize)
transition.updateFrame(view: starView, frame: starFrame)
@@ -4248,14 +4263,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.starView = nil
starView.removeFromSuperview()
}
-
+
let autoremoveTimeoutFraction: CGFloat
if online {
autoremoveTimeoutFraction = 0.0
} else {
autoremoveTimeoutFraction = 1.0 - onlineInlineNavigationFraction
}
-
+
if let autoremoveTimeout = autoremoveTimeout {
let avatarTimerBadge: AvatarBadgeView
var avatarTimerTransition = transition
@@ -4276,17 +4291,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
avatarTimerTransition.updatePosition(layer: avatarTimerBadge.layer, position: avatarBadgeFrame.center)
avatarTimerTransition.updateBounds(layer: avatarTimerBadge.layer, bounds: CGRect(origin: CGPoint(), size: avatarBadgeFrame.size))
avatarTimerTransition.updateTransformScale(layer: avatarTimerBadge.layer, scale: autoremoveTimeoutFraction * 1.0 + (1.0 - autoremoveTimeoutFraction) * 0.00001)
-
+
strongSelf.avatarNode.badgeView = avatarTimerBadge
} else if let avatarTimerBadge = strongSelf.avatarTimerBadge {
strongSelf.avatarTimerBadge = nil
strongSelf.avatarNode.badgeView = nil
avatarTimerBadge.removeFromSuperview()
}
-
+
let _ = measureApply()
let _ = dateApply()
-
+
var currentTextSnapshotView: UIView?
if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() {
if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() {
@@ -4299,7 +4314,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
-
+
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.interaction.animationCache,
@@ -4307,22 +4322,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: synchronousLoads
))
-
+
var topForumTopicRect = authorApply()
if !isFirstForumThreadSelectable {
topForumTopicRect = nil
}
-
+
let _ = titleApply()
let _ = badgeApply(animateBadges, !isMuted)
let _ = mentionBadgeApply(animateBadges, true)
let _ = onlineApply(animateContent && animateOnline)
-
+
var dateFrame = CGRect(origin: CGPoint(x: contentRect.maxX - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil {
dateFrame.origin.x -= 10.0
-
+
let dateDisclosureIconView: UIImageView
if let current = strongSelf.dateDisclosureIconView {
dateDisclosureIconView = current
@@ -4342,13 +4357,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.dateDisclosureIconView = nil
dateDisclosureIconView.removeFromSuperview()
}
-
+
transition.updateFrame(node: strongSelf.dateNode, frame: dateFrame)
-
+
var statusOffset: CGFloat = 0.0
if let dateIconImage {
statusOffset += 2.0 + dateIconImage.size.width + 4.0
-
+
let dateStatusIconNode: ASImageNode
if let current = strongSelf.dateStatusIconNode {
dateStatusIconNode = current
@@ -4358,70 +4373,70 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.mainContentContainerNode.addSubnode(dateStatusIconNode)
}
dateStatusIconNode.image = dateIconImage
-
+
var dateStatusX: CGFloat = contentRect.origin.x
dateStatusX += contentRect.size.width
dateStatusX += -dateLayout.size.width - 4.0 - dateIconImage.size.width
-
+
var dateStatusY: CGFloat = contentRect.origin.y + 2.0 + UIScreenPixel
dateStatusY += -UIScreenPixel + floor((dateLayout.size.height - dateIconImage.size.height) / 2.0)
-
+
transition.updateFrame(node: dateStatusIconNode, frame: CGRect(origin: CGPoint(x: dateStatusX, y: dateStatusY), size: dateIconImage.size))
} else if let dateStatusIconNode = strongSelf.dateStatusIconNode {
strongSelf.dateStatusIconNode = nil
dateStatusIconNode.removeFromSupernode()
}
-
+
let statusSize = CGSize(width: 24.0, height: 24.0)
-
+
var statusX: CGFloat = contentRect.origin.x
statusX += contentRect.size.width
statusX += -dateLayout.size.width - statusSize.width - statusOffset
-
+
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: statusX, y: contentRect.origin.y + 2.0 - UIScreenPixel + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
-
+
let rightAccessoryVerticalOffset: CGFloat = floorToScreenPixels(-4.0 * min(1.0, item.presentationData.fontSize.itemListBaseFontSize / 17.0))
var nextBadgeX: CGFloat = contentRect.maxX
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height)
-
+
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
nextBadgeX -= badgeLayout.width + 6.0
}
-
+
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
-
+
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
nextBadgeX -= mentionBadgeLayout.width + 6.0
}
-
+
if let currentPinnedIconImage = currentPinnedIconImage {
strongSelf.pinnedIconNode.image = currentPinnedIconImage
strongSelf.pinnedIconNode.isHidden = false
-
+
let pinnedIconSize = currentPinnedIconImage.size
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height)
-
+
strongSelf.pinnedIconNode.frame = pinnedIconFrame
nextBadgeX -= pinnedIconSize.width + 6.0
} else {
strongSelf.pinnedIconNode.image = nil
strongSelf.pinnedIconNode.isHidden = true
}
-
+
if let (actionButtonTitleNodeLayout, apply) = actionButtonTitleNodeLayoutAndApply {
let actionButtonSideInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 12.0 / 17.0)
let actionButtonTopInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 5.0 / 17.0)
let actionButtonBottomInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0)
-
+
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset)
var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height)
actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0))
actionButtonFrame.origin.y += 4.0
-
+
let actionButtonNode: HighlightableButtonNode
var animateActionButtonIn = false
if let current = strongSelf.actionButtonNode {
@@ -4433,7 +4448,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
}
-
+
let actionButtonBackgroundView: UIImageView
if let current = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView = current
@@ -4442,20 +4457,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.actionButtonBackgroundView = actionButtonBackgroundView
actionButtonNode.view.addSubview(actionButtonBackgroundView)
}
-
+
if actionButtonBackgroundView.image?.size.height != actionButtonSize.height {
actionButtonBackgroundView.image = generateStretchableFilledCircleImage(diameter: actionButtonSize.height, color: .white)?.withRenderingMode(.alwaysTemplate)
}
-
+
actionButtonBackgroundView.tintColor = theme.unreadBadgeActiveBackgroundColor
-
+
let actionButtonTitleNode = apply()
if strongSelf.actionButtonTitleNode !== actionButtonTitleNode {
strongSelf.actionButtonTitleNode?.removeFromSupernode()
strongSelf.actionButtonTitleNode = actionButtonTitleNode
actionButtonNode.addSubnode(actionButtonTitleNode)
}
-
+
actionButtonNode.isUserInteractionEnabled = true
actionButtonNode.frame = actionButtonFrame
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
@@ -4464,7 +4479,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
actionButtonNode.alpha = 0.0
}
transition.updateAlpha(node: actionButtonNode, alpha: 1.0)
-
+
nextBadgeX -= actionButtonSize.width + 6.0
} else {
if let actionButtonNode = strongSelf.actionButtonNode {
@@ -4492,7 +4507,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
var titleOffset: CGFloat = titleLeftOffset
if let currentSecretIconImage = currentSecretIconImage {
let iconNode: ASImageNode
@@ -4513,15 +4528,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.secretIconNode = nil
secretIconNode.removeFromSupernode()
}
-
+
let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel))
let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
-
+
let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0), size: authorLayout)
strongSelf.authorNode.frame = authorNodeFrame
let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0 + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size)
-
+
if let topForumTopicRect, !isSearching {
let compoundHighlightingNode: LinkHighlightingNode
if let current = strongSelf.compoundHighlightingNode {
@@ -4533,7 +4548,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.compoundHighlightingNode = compoundHighlightingNode
strongSelf.mainContentContainerNode.insertSubnode(compoundHighlightingNode, at: 0)
}
-
+
let compoundTextButtonNode: HighlightTrackingButtonNode
if let current = strongSelf.compoundTextButtonNode {
compoundTextButtonNode = current
@@ -4554,7 +4569,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
compoundHighlightingNode.alpha = 1.0
compoundHighlightingNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
-
+
let prevAlpha = strongSelf.textNode.textNode.alpha
strongSelf.textNode.textNode.alpha = strongSelf.authorNode.alpha
strongSelf.textNode.textNode.layer.animateAlpha(from: prevAlpha, to: strongSelf.authorNode.alpha, duration: 0.2)
@@ -4562,7 +4577,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
var topRect = topForumTopicRect
topRect.origin.x -= 1.0
topRect.size.width += 2.0
@@ -4570,28 +4585,28 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textRect.origin.x = topRect.minX
textRect.size.height -= 1.0
textRect.size.width += 16.0
-
+
compoundHighlightingNode.frame = CGRect(origin: CGPoint(x: authorNodeFrame.minX, y: authorNodeFrame.minY), size: CGSize(width: textNodeFrame.maxX - authorNodeFrame.minX, height: textNodeFrame.maxY - authorNodeFrame.minY))
-
+
let midY = floor((topForumTopicRect.minY + textRect.maxY) / 2.0) + 1.0
-
+
let finalTopRect = CGRect(origin: topRect.origin, size: CGSize(width: topRect.width, height: midY - topRect.minY))
var finalBottomRect = CGRect(origin: CGPoint(x: textRect.minX, y: midY), size: CGSize(width: textRect.width, height: textRect.maxY - midY))
if finalBottomRect.maxX < finalTopRect.maxX && abs(finalBottomRect.maxX - finalTopRect.maxX) < 5.0 {
finalBottomRect.size.width = finalTopRect.maxX - finalBottomRect.minX
}
-
+
compoundHighlightingNode.inset = 0.0
compoundHighlightingNode.outerRadius = floor(finalBottomRect.height * 0.5)
compoundHighlightingNode.innerRadius = 4.0
-
+
compoundHighlightingNode.updateRects([
finalTopRect,
finalBottomRect
], color: theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1))
-
+
transition.updateFrame(node: compoundTextButtonNode, frame: compoundHighlightingNode.frame)
-
+
if let textArrowImage = textArrowImage {
let textArrowNode: ASImageNode
if let current = strongSelf.textArrowNode {
@@ -4623,7 +4638,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textArrowNode.removeFromSupernode()
}
}
-
+
if let compoundTextButtonNode = strongSelf.compoundTextButtonNode {
if strongSelf.textNode.textNode.supernode !== compoundTextButtonNode {
compoundTextButtonNode.addSubnode(strongSelf.textNode.textNode)
@@ -4632,7 +4647,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
strongSelf.textNode.textNode.frame = textNodeFrame.offsetBy(dx: -compoundTextButtonNode.frame.minX, dy: -compoundTextButtonNode.frame.minY)
-
+
strongSelf.authorNode.assignParentNode(parentNode: compoundTextButtonNode)
} else {
if strongSelf.textNode.textNode.supernode !== strongSelf.mainContentContainerNode {
@@ -4642,17 +4657,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
strongSelf.textNode.textNode.frame = textNodeFrame
-
+
strongSelf.authorNode.assignParentNode(parentNode: nil)
}
-
+
if let currentTextSnapshotView {
transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin)
}
-
+
if let trailingTextBadgeLayoutAndApply {
let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel)
-
+
var badgeFrame: CGRect
if textLayout.numberOfLines > 1 {
badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth + 4.0, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize)
@@ -4660,11 +4675,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size)
badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize)
}
-
+
if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 {
badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width
}
-
+
let trailingTextBadgeBackground: UIImageView
if let current = strongSelf.trailingTextBadgeBackground {
trailingTextBadgeBackground = current
@@ -4674,20 +4689,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground)
}
trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1)
-
+
trailingTextBadgeBackground.frame = badgeFrame
-
+
let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size)
let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1()
if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode {
strongSelf.trailingTextBadgeNode?.removeFromSupernode()
strongSelf.trailingTextBadgeNode = trailingTextBadgeNode
-
+
strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode)
-
+
trailingTextBadgeNode.layer.anchorPoint = CGPoint()
}
-
+
trailingTextBadgeNode.frame = trailingTextBadgeFrame
} else {
if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode {
@@ -4699,12 +4714,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
trailingTextBadgeBackground.removeFromSuperview()
}
}
-
+
if !itemTags.isEmpty {
let sizeFactor = item.presentationData.fontSize.itemListBaseFontSize / 17.0
-
+
let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + measureLayout.size.height * 2.0 + floorToScreenPixels(2.0 * sizeFactor)), size: CGSize(width: contentRect.width, height: floorToScreenPixels(20.0 * sizeFactor)))
-
+
var itemTagListTransition = transition
let itemTagList: ComponentView
if let current = strongSelf.itemTagList {
@@ -4730,7 +4745,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
itemTagListView.isUserInteractionEnabled = false
strongSelf.mainContentContainerNode.view.addSubview(itemTagListView)
}
-
+
itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame)
itemTagListView.isVisible = strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji
}
@@ -4740,7 +4755,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
itemTagList.view?.removeFromSuperview()
}
}
-
+
if !textLayout.spoilers.isEmpty {
let dustNode: InvisibleInkDustNode
if let current = strongSelf.dustNode {
@@ -4749,17 +4764,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
dustNode.isUserInteractionEnabled = false
strongSelf.dustNode = dustNode
-
+
strongSelf.textNode.textNode.supernode?.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode.textNode)
}
dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, textColor: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
-
+
} else if let dustNode = strongSelf.dustNode {
strongSelf.dustNode = nil
dustNode.removeFromSupernode()
}
-
+
var animateInputActivitiesFrame = false
let inputActivities = inputActivities?.filter({
switch $0.1 {
@@ -4769,21 +4784,21 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
return true
}
})
-
+
if let inputActivities = inputActivities, !inputActivities.isEmpty {
if strongSelf.inputActivitiesNode.supernode == nil {
strongSelf.mainContentContainerNode.addSubnode(strongSelf.inputActivitiesNode)
} else {
animateInputActivitiesFrame = true
}
-
+
if strongSelf.inputActivitiesNode.alpha.isZero {
strongSelf.inputActivitiesNode.alpha = 1.0
strongSelf.textNode.textNode.alpha = 0.0
strongSelf.authorNode.alpha = 0.0
strongSelf.compoundHighlightingNode?.alpha = 0.0
strongSelf.forwardedIconNode.alpha = 0.0
-
+
if animated || animateContent {
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
@@ -4827,12 +4842,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
inputActivitiesApply?()
-
+
var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: 1.0 + floor((measureLayout.size.height - contentImageSize.height) / 2.0))
-
+
let messageTypeIconImage = currentMessageTypeIcon
let messageTypeIconOffset = CGPoint(x: mediaPreviewOffset.x + currentMessageTypeIconOffset.x, y: mediaPreviewOffset.y + currentMessageTypeIconOffset.y)
-
+
if let messageTypeIconImage {
strongSelf.forwardedIconNode.image = messageTypeIconImage
if strongSelf.forwardedIconNode.supernode == nil {
@@ -4844,13 +4859,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if strongSelf.forwardedIconNode.supernode != nil {
strongSelf.forwardedIconNode.removeFromSupernode()
}
-
+
var validMediaIds: [EngineMedia.Id] = []
for spec in contentImageSpecs {
let message = spec.message
let media = spec.media
let mediaSize = spec.size
-
+
var mediaId = media.id
if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action {
mediaId = image?.id
@@ -4888,15 +4903,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
strongSelf.currentMediaPreviewSpecs = contentImageSpecs
strongSelf.currentTextLeftCutout = textLeftCutout
-
+
if let avatarContentImageSpec {
strongSelf.avatarNode.isHidden = true
-
+
if let previous = strongSelf.avatarMediaNode, previous.media != avatarContentImageSpec.media {
strongSelf.avatarMediaNode = nil
previous.removeFromSupernode()
}
-
+
var avatarMediaNodeTransition = transition
let avatarMediaNode: ChatListMediaPreviewNode
if let current = strongSelf.avatarMediaNode {
@@ -4907,29 +4922,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarMediaNode = avatarMediaNode
strongSelf.contextContainer.addSubnode(avatarMediaNode)
}
-
+
avatarMediaNodeTransition.updateFrame(node: avatarMediaNode, frame: avatarFrame)
avatarMediaNode.updateLayout(size: avatarFrame.size, synchronousLoads: synchronousLoads)
} else {
strongSelf.avatarNode.isHidden = false
-
+
if let avatarMediaNode = strongSelf.avatarMediaNode {
strongSelf.avatarMediaNode = nil
avatarMediaNode.removeFromSupernode()
}
}
-
+
if !contentDelta.x.isZero || !contentDelta.y.isZero {
let titlePosition = strongSelf.titleNode.position
transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDelta.x, y: titlePosition.y - contentDelta.y))
-
+
if strongSelf.textNode.textNode.supernode === strongSelf.mainContentContainerNode {
transition.animatePositionAdditive(node: strongSelf.textNode.textNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
if let dustNode = strongSelf.dustNode {
transition.animatePositionAdditive(node: dustNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
}
}
-
+
let authorPosition = strongSelf.authorNode.position
transition.animatePosition(node: strongSelf.authorNode, from: CGPoint(x: authorPosition.x - contentDelta.x, y: authorPosition.y - contentDelta.y))
if let compoundHighlightingNode = strongSelf.compoundHighlightingNode {
@@ -4937,13 +4952,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.animatePosition(node: compoundHighlightingNode, from: CGPoint(x: compoundHighlightingPosition.x - contentDelta.x, y: compoundHighlightingPosition.y - contentDelta.y))
}
}
-
+
if crossfadeContent {
strongSelf.authorNode.recursivelyEnsureDisplaySynchronously(true)
strongSelf.titleNode.recursivelyEnsureDisplaySynchronously(true)
strongSelf.textNode.textNode.recursivelyEnsureDisplaySynchronously(true)
}
-
+
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.trailingLineWidth + 3.0 + titleOffset
let lastLineRect: CGRect
if let rect = titleLayout.linesRects().last {
@@ -4951,7 +4966,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
lastLineRect = CGRect(origin: CGPoint(), size: titleLayout.size)
}
-
+
if let currentStatusIconContent {
let statusIconView: ComponentHostView
if let current = strongSelf.statusIconView {
@@ -4961,7 +4976,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusIconView = statusIconView
strongSelf.mainContentContainerNode.view.addSubview(statusIconView)
}
-
+
let statusIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: item.interaction.animationCache,
@@ -4972,7 +4987,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
action: nil
)
strongSelf.statusIconComponent = statusIconComponent
-
+
let iconOrigin: CGFloat = nextTitleIconOrigin
let containerSize = CGSize(width: 20.0, height: 20.0)
let iconSize = statusIconView.update(
@@ -4987,7 +5002,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusIconView = nil
statusIconView.removeFromSuperview()
}
-
+
if let currentCredibilityIconContent {
let credibilityIconView: ComponentHostView
if let current = strongSelf.credibilityIconView {
@@ -4997,7 +5012,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.credibilityIconView = credibilityIconView
strongSelf.mainContentContainerNode.view.addSubview(credibilityIconView)
}
-
+
let credibilityIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: item.interaction.animationCache,
@@ -5007,7 +5022,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
action: nil
)
strongSelf.credibilityIconComponent = credibilityIconComponent
-
+
let iconOrigin: CGFloat = nextTitleIconOrigin
let containerSize: CGSize
if case .verified = currentCredibilityIconContent {
@@ -5027,7 +5042,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.credibilityIconView = nil
credibilityIconView.removeFromSuperview()
}
-
+
if let currentVerifiedIconContent {
let verifiedIconView: ComponentHostView
if let current = strongSelf.verifiedIconView {
@@ -5037,7 +5052,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.verifiedIconView = verifiedIconView
strongSelf.mainContentContainerNode.view.addSubview(verifiedIconView)
}
-
+
let verifiedIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: item.interaction.animationCache,
@@ -5047,7 +5062,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
action: nil
)
strongSelf.verifiedIconComponent = verifiedIconComponent
-
+
let iconOrigin: CGFloat
if case .animation = currentVerifiedIconContent {
iconOrigin = contentRect.origin.x
@@ -5055,7 +5070,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
iconOrigin = nextTitleIconOrigin
}
let containerSize = CGSize(width: 16.0, height: 16.0)
-
+
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
@@ -5063,11 +5078,51 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
containerSize: containerSize
)
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize))
+ if case .animation = currentVerifiedIconContent {
+ } else {
+ nextTitleIconOrigin += verifiedIconView.bounds.width + 4.0
+ }
} else if let verifiedIconView = strongSelf.verifiedIconView {
strongSelf.verifiedIconView = nil
verifiedIconView.removeFromSuperview()
}
-
+
+ // WinterGram: snowflake badge for official peers, placed after the premium/emoji status.
+ if currentWinterGramOfficial {
+ let winterGramIconView: ComponentHostView
+ if let current = strongSelf.winterGramIconView {
+ winterGramIconView = current
+ } else {
+ winterGramIconView = ComponentHostView()
+ strongSelf.winterGramIconView = winterGramIconView
+ strongSelf.mainContentContainerNode.view.addSubview(winterGramIconView)
+ }
+
+ let winterGramIconComponent = EmojiStatusComponent(
+ context: item.context,
+ animationCache: item.interaction.animationCache,
+ animationRenderer: item.interaction.animationRenderer,
+ content: currentWinterGramOfficial ? .winterGramBadge(backplateColor: winterGramBadgeBackplateColor(theme: item.presentationData.theme)) : EmojiStatusComponent.Content.none,
+ isVisibleForAnimations: false,
+ action: nil
+ )
+ strongSelf.winterGramIconComponent = winterGramIconComponent
+
+ let iconOrigin: CGFloat = nextTitleIconOrigin
+ let containerSize = CGSize(width: 18.0, height: 18.0)
+ let iconSize = winterGramIconView.update(
+ transition: .immediate,
+ component: AnyComponent(winterGramIconComponent),
+ environment: {},
+ containerSize: containerSize
+ )
+ transition.updateFrame(view: winterGramIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize))
+ nextTitleIconOrigin += winterGramIconView.bounds.width + 4.0
+ } else if let winterGramIconView = strongSelf.winterGramIconView {
+ strongSelf.winterGramIconView = nil
+ winterGramIconView.removeFromSuperview()
+ }
+
if let currentMutedIconImage = currentMutedIconImage {
strongSelf.mutedIconNode.image = currentMutedIconImage
strongSelf.mutedIconNode.isHidden = false
@@ -5077,7 +5132,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.mutedIconNode.image = nil
strongSelf.mutedIconNode.isHidden = true
}
-
+
if let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayoutAndApply {
let titleBadgeNode = titleBadgeApply()
let backgroundView: UIImageView
@@ -5086,7 +5141,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
backgroundView = UIImageView(image: generateStretchableFilledCircleImage(radius: 4.0, color: .white)?.withRenderingMode(.alwaysTemplate))
strongSelf.titleBadge = (backgroundView, titleBadgeNode)
-
+
strongSelf.mainContentContainerNode.view.addSubview(backgroundView)
strongSelf.mainContentContainerNode.addSubnode(titleBadgeNode)
}
@@ -5097,7 +5152,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let titleBadgeFrame = CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: titleFrame.minY + floor((titleFrame.height - titleBadgeLayout.size.height) * 0.5)), size: titleBadgeLayout.size)
nextTitleIconOrigin += titleBadgeLayout.size.width + 4.0
transition.updateFrame(node: titleBadgeNode, frame: titleBadgeFrame)
-
+
var titleBadgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -4.0, dy: -2.0)
titleBadgeBackgroundFrame.size.height -= 1.0
transition.updateFrame(view: backgroundView, frame: titleBadgeBackgroundFrame)
@@ -5111,7 +5166,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleBadge.backgroundView.removeFromSuperview()
titleBadge.textNode.removeFromSupernode()
}
-
+
let leftSeparatorInset: CGFloat
let rightSeparatorInset: CGFloat
if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault {
@@ -5124,20 +5179,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
leftSeparatorInset = editingOffset + leftInset + rawContentRect.origin.x
rightSeparatorInset = 16.0
}
-
+
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftSeparatorInset, y: layoutOffset + itemHeight - separatorHeight), size: CGSize(width: params.width - leftSeparatorInset - rightSeparatorInset, height: separatorHeight)))
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
strongSelf.updateSeparatorAlpha(transition: transition, inlineNavigationProgress: inlineNavigationLocation.progress)
} else {
strongSelf.updateSeparatorAlpha(transition: transition)
}
-
+
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.hideSeparator {
strongSelf.separatorNode.isHidden = true
}
}
-
+
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: itemHeight)))
let backgroundColor: UIColor
let highlightedBackgroundColor: UIColor
@@ -5160,30 +5215,30 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
}
-
+
if animated {
transition.updateBackgroundColor(node: strongSelf.backgroundNode, color: backgroundColor)
} else {
strongSelf.backgroundNode.backgroundColor = backgroundColor
}
-
+
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: 1.0 - inlineNavigationLocation.progress)
} else {
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: 1.0)
}
-
+
strongSelf.highlightedBackgroundNode.backgroundColor = highlightedBackgroundColor
let topNegativeInset: CGFloat = 0.0
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: strongSelf.revealOffset, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
transition.updateCornerRadius(node: strongSelf.highlightedBackgroundNode, cornerRadius: strongSelf.isRevealOptionsActive ? 26.0 : 0.0)
-
+
if let peerPresence = peerPresence {
strongSelf.peerPresenceManager?.reset(presence: EnginePeer.Presence(status: peerPresence.status, lastActivity: 0), isOnline: online)
}
-
+
strongSelf.updateLayout(size: CGSize(width: layout.contentSize.width, height: itemHeight), leftInset: params.leftInset, rightInset: params.rightInset)
-
+
if item.editing {
strongSelf.setRevealOptions((left: [], right: []), enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
} else {
@@ -5192,10 +5247,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if !strongSelf.customAnimationInProgress {
strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: true)
}
-
+
strongSelf.view.accessibilityLabel = strongSelf.accessibilityLabel
strongSelf.view.accessibilityValue = strongSelf.accessibilityValue
-
+
if !customActions.isEmpty {
strongSelf.view.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in
return ChatListItemAccessibilityCustomAction(name: action.name, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)), key: action.key)
@@ -5203,9 +5258,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
strongSelf.view.accessibilityCustomActions = nil
}
-
+
strongSelf.avatarTapRecognizer?.isEnabled = item.interaction.inlineNavigationLocation == nil
-
+
if case .loading = item.content {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
@@ -5219,27 +5274,27 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
-
+
var shapes: [ShimmerEffectNode.Shape] = []
-
+
let titleLineWidth: CGFloat = 180.0
let dateLineWidth: CGFloat = 36.0
let textFirstLineWidth: CGFloat = 240.0
let textSecondLineWidth: CGFloat = 200.0
let lineDiameter: CGFloat = 10.0
-
+
shapes.append(.circle(avatarFrame))
-
+
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
-
+
let textFrame = strongSelf.textNode.textNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: textFrame.minX, y: textFrame.minY + 7.0), width: textFirstLineWidth, diameter: lineDiameter))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: textFrame.minX, y: textFrame.minY + 7.0 + lineDiameter + 9.0), width: textSecondLineWidth, diameter: lineDiameter))
-
+
let dateFrame = strongSelf.dateNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: dateFrame.maxX - dateLineWidth, y: dateFrame.minY + 3.0), width: dateLineWidth, diameter: lineDiameter))
-
+
shimmerNode.update(backgroundColor: item.presentationData.theme.list.plainBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
@@ -5249,7 +5304,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
})
}
}
-
+
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
@@ -5258,7 +5313,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
-
+
@objc private func compoundTextButtonPressed() {
guard let item else {
return
@@ -5274,7 +5329,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id)
}
-
+
@objc private func actionButtonPressed() {
guard let item else {
return
@@ -5286,11 +5341,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
item.interaction.openWebApp(user)
}
}
-
+
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
-
+
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.clipsToBounds = true
if self.skipFadeout {
@@ -5299,7 +5354,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
-
+
override public func headers() -> [ListViewItemHeader]? {
if let item = self.layoutParams?.0 {
return item.header.flatMap { [$0] }
@@ -5307,17 +5362,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
return nil
}
}
-
+
private func updateVideoVisibility() {
let isVisible = self.visibilityStatus && self.trackingIsInHierarchy
self.avatarVideoNode?.updateVisibility(isVisible)
-
+
if let videoNode = self.avatarVideoNode {
videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate)
videoNode.frame = self.avatarNode.bounds
}
}
-
+
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
@@ -5338,14 +5393,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.updateSeparatorAlpha(transition: transition)
}
-
+
override public func touchesToOtherItemsPrevented() {
super.touchesToOtherItemsPrevented()
if let item = self.item {
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
}
}
-
+
override public func revealOptionsInteractivelyOpened() {
if let item = self.item {
switch item.index {
@@ -5356,7 +5411,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
override public func revealOptionsInteractivelyClosed() {
if let item = self.item {
switch item.index {
@@ -5367,12 +5422,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
guard let item = self.item else {
return
}
-
+
var close = true
if case let .chatList(index) = item.index {
switch option.key {
@@ -5498,14 +5553,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.revealOptionsInteractivelyClosed()
}
}
-
+
override public func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) {
return true
}
return false
}
-
+
func flashHighlight() {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
@@ -5516,17 +5571,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self?.updateIsHighlighted(transition: .immediate)
})
}
-
+
func playArchiveAnimation() {
guard let item = self.item, case .groupReference = item.content else {
return
}
self.avatarNode.playArchiveAnimation()
}
-
+
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
-
+
if let item = self.item {
if case .groupReference = item.content {
self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, currentValue - (self.currentItemHeight ?? 0.0), 0.0)
@@ -5537,25 +5592,25 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
if let action = action as? ChatListItemAccessibilityCustomAction {
self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, iconColor: .white, textColor: .white), animated: false)
}
}
-
+
override public func snapshotForReordering() -> UIView? {
self.backgroundNode.alpha = 0.9
let result = self.view.snapshotContentTree()
self.backgroundNode.alpha = 1.0
return result
}
-
+
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let item = self.item, self.frame.height > 0.0 else {
return nil
}
-
+
if let compoundTextButtonNode = self.compoundTextButtonNode, let compoundHighlightingNode = self.compoundHighlightingNode, compoundHighlightingNode.alpha != 0.0 {
let localPoint = self.view.convert(point, to: compoundHighlightingNode.view)
var matches = false
@@ -5569,7 +5624,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
return compoundTextButtonNode.view
}
}
-
+
if let _ = item.interaction.inlineNavigationLocation {
} else {
if self.avatarNode.storyStats != nil {
@@ -5578,10 +5633,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
-
+
return super.hitTest(point, with: event)
}
-
+
@objc private func avatarStoryTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let item = self.item else {
@@ -5602,27 +5657,27 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
private class StarView: UIView {
let outline = SimpleLayer()
let foreground = SimpleLayer()
-
+
var outlineColor: UIColor = .white {
didSet {
self.outline.layerTintColor = self.outlineColor.cgColor
}
}
-
+
override init(frame: CGRect) {
self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage
self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage
-
+
super.init(frame: frame)
-
+
self.layer.addSublayer(self.outline)
self.layer.addSublayer(self.foreground)
}
-
+
required init?(coder: NSCoder) {
preconditionFailure()
}
-
+
func setOutlineColor(_ color: UIColor, transition: ContainedViewLayoutTransition) {
if case let .animated(duration, curve) = transition, color != self.outlineColor {
let snapshotLayer = SimpleLayer()
@@ -5636,7 +5691,7 @@ private class StarView: UIView {
}
self.outlineColor = color
}
-
+
override func layoutSubviews() {
self.outline.frame = self.bounds
self.foreground.frame = self.bounds
diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift
index 2dc44bb69b..981671ffea 100644
--- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift
+++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift
@@ -45,16 +45,16 @@ struct ChatListNodeListViewTransition {
final class ChatListHighlightedLocation: Equatable {
let location: ChatLocation
let progress: CGFloat
-
+
init(location: ChatLocation, progress: CGFloat) {
self.location = location
self.progress = progress
}
-
+
func withUpdatedProgress(_ progress: CGFloat) -> ChatListHighlightedLocation {
return ChatListHighlightedLocation(location: location, progress: progress)
}
-
+
static func ==(lhs: ChatListHighlightedLocation, rhs: ChatListHighlightedLocation) -> Bool {
if lhs.location != rhs.location {
return false
@@ -71,7 +71,7 @@ public final class ChatListNodeInteraction {
case peerId(EnginePeer.Id)
case peer(EnginePeer)
}
-
+
let activateSearch: () -> Void
let peerSelected: (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?, Bool) -> Void
let disabledPeerSelected: (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void
@@ -117,18 +117,18 @@ public final class ChatListNodeInteraction {
let openAdInfo: (ASDisplayNode, AdPeer) -> Void
let openAccountFreezeInfo: () -> Void
let openUrl: (String) -> Void
-
+
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
-
+
var isSearchMode: Bool = false
-
+
var isInlineMode: Bool = false
var inlineNavigationLocation: ChatListHighlightedLocation?
-
+
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
-
+
public init(
context: AccountContext,
animationCache: AnimationCache,
@@ -233,15 +233,15 @@ public final class ChatListNodePeerInputActivities {
public struct ItemId: Hashable {
public var peerId: EnginePeer.Id
public var threadId: Int64?
-
+
public init(peerId: EnginePeer.Id, threadId: Int64?) {
self.peerId = peerId
self.threadId = threadId
}
}
-
+
public let activities: [ItemId: [(EnginePeer, PeerInputActivity)]]
-
+
public init(activities: [ItemId: [(EnginePeer, PeerInputActivity)]]) {
self.activities = activities
}
@@ -263,23 +263,23 @@ public struct ChatListNodeState: Equatable {
public struct StoryState: Equatable {
public var stats: EngineChatList.StoryStats
public var hasUnseenCloseFriends: Bool
-
+
public init(stats: EngineChatList.StoryStats, hasUnseenCloseFriends: Bool) {
self.stats = stats
self.hasUnseenCloseFriends = hasUnseenCloseFriends
}
}
-
+
public struct ItemId: Hashable {
public var peerId: EnginePeer.Id
public var threadId: Int64?
-
+
public init(peerId: EnginePeer.Id, threadId: Int64?) {
self.peerId = peerId
self.threadId = threadId
}
}
-
+
public var presentationData: ChatListPresentationData
public var editing: Bool
public var peerIdWithRevealedOptions: ItemId?
@@ -294,7 +294,7 @@ public struct ChatListNodeState: Equatable {
public var selectedPeerMap: [EnginePeer.Id: EnginePeer]
public var selectedThreadIds: Set
public var archiveStoryState: StoryState?
-
+
public init(
presentationData: ChatListPresentationData,
editing: Bool,
@@ -326,7 +326,7 @@ public struct ChatListNodeState: Equatable {
self.selectedThreadIds = selectedThreadIds
self.archiveStoryState = archiveStoryState
}
-
+
public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool {
if lhs.presentationData !== rhs.presentationData {
return false
@@ -444,7 +444,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
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(
@@ -514,7 +514,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
} else {
enabled = false
}
-
+
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
} else {
@@ -577,7 +577,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
}
-
+
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _, _, _, _):
@@ -594,7 +594,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
default:
break
}
-
+
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if displayPresence, let presence = presence {
@@ -605,14 +605,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
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, _, _):
@@ -620,7 +620,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
case .chatList:
break
}
-
+
var isForum = false
if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum {
isForum = true
@@ -628,7 +628,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
enabled = false
}
}
-
+
var selectable = editing
if case .chatList = mode {
if isForum {
@@ -672,10 +672,10 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
if let peer = peer.peers[peer.peerId] {
chatPeer = 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,
@@ -740,12 +740,12 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
), 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(
@@ -805,7 +805,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
let forumTopicData = peerEntry.forumTopicData
let topForumTopicItems = peerEntry.topForumTopicItems
let revealed = peerEntry.revealed
-
+
switch mode {
case .chatList:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
@@ -875,7 +875,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
} else {
enabled = false
}
-
+
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
} else {
@@ -889,7 +889,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
}
}
-
+
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _, _, _, _):
@@ -906,7 +906,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
default:
break
}
-
+
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if displayPresence, let presence = presence {
@@ -917,14 +917,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
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, _, _):
@@ -932,7 +932,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
case .chatList:
break
}
-
+
var isForum = false
if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum {
isForum = true
@@ -940,14 +940,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
enabled = false
}
}
-
+
var selectable = editing
if case .chatList = mode {
if isForum {
selectable = false
}
}
-
+
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,
@@ -984,10 +984,10 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
-
+
let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer)
let status: ContactsPeerItemStatus = .none
-
+
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,
@@ -1052,12 +1052,12 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
), 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 ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
@@ -1143,7 +1143,7 @@ private func mappedChatListNodeViewListTransition(context: AccountContext, nodeI
private final class ChatListOpaqueTransactionState {
let chatListView: ChatListNodeView
-
+
init(chatListView: ChatListNodeView) {
self.chatListView = chatListView
}
@@ -1176,26 +1176,26 @@ public final class ChatListNode: ListViewImpl {
case peer(EnginePeer.Id)
case archive
}
-
+
private let fillPreloadItems: Bool
private let context: AccountContext
private let location: ChatListControllerLocation
private let mode: ChatListNodeMode
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
-
+
private let _ready = ValuePromise()
private var didSetReady = false
public var ready: Signal {
return _ready.get()
}
-
+
private let _contentsReady = ValuePromise()
private var didSetContentsReady = false
public var contentsReady: Signal {
return _contentsReady.get()
}
-
+
public var peerSelected: ((EnginePeer, Int64?, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
public var disabledPeerSelected: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)?
public var additionalCategorySelected: ((Int) -> Void)?
@@ -1222,9 +1222,9 @@ public final class ChatListNode: ListViewImpl {
public var openPhotoSetup: (() -> Void)?
public var openAdInfo: ((ASDisplayNode, AdPeer) -> Void)?
public var openAccountFreezeInfo: (() -> Void)?
-
+
private var theme: PresentationTheme
-
+
private let viewProcessingQueue = Queue()
private var chatListView: ChatListNodeView?
public var entriesCount: Int {
@@ -1251,21 +1251,21 @@ public final class ChatListNode: ListViewImpl {
}
}
public private(set) var interaction: ChatListNodeInteraction?
-
+
private var dequeuedInitialTransitionOnLayout = false
private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)?
-
+
public private(set) var currentState: ChatListNodeState
private let statePromise: ValuePromise
public var state: Signal {
return self.statePromise.get()
}
-
+
private var currentLocation: ChatListNodeLocation?
public private(set) var chatListFilter: ChatListFilter? {
didSet {
self.chatListFilterValue.set(.single(self.chatListFilter))
-
+
if self.chatListFilter != oldValue {
self.setChatListLocation(.initial(count: 50, filter: self.chatListFilter))
}
@@ -1285,12 +1285,12 @@ public final class ChatListNode: ListViewImpl {
private let chatListLocation = ValuePromise()
private let chatListDisposable = MetaDisposable()
private var activityStatusesDisposable: Disposable?
-
+
private let scrollToTopOptionPromise = Promise(.none)
public var scrollToTopOption: Signal {
return self.scrollToTopOptionPromise.get()
}
-
+
private let scrolledAtTop = ValuePromise(true)
private var scrolledAtTopValue: Bool = true {
didSet {
@@ -1299,63 +1299,63 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
public private(set) var pinnedScrollFraction: CGFloat = 0.0
public var pinnedHeaderDisplayFractionUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var contentScrollingEnded: ((ListView) -> Bool)?
public var didBeginInteractiveDragging: ((ListView) -> Void)?
-
+
public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
private var currentIsEmptyState: ChatListNodeEmptyState?
-
+
public var canExpandHiddenItems: (() -> Bool)?
-
+
public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
-
+
private let currentRemovingItemId = Atomic(value: nil)
public func setCurrentRemovingItemId(_ itemId: ChatListNodeState.ItemId?) {
let _ = self.currentRemovingItemId.swap(itemId)
}
-
+
private var hapticFeedback: HapticFeedback?
-
+
let preloadItems = Promise<[ChatHistoryPreloadItem]>([])
-
+
var didBeginSelectingChats: (() -> Void)?
public var selectionCountChanged: ((Int) -> Void)?
-
+
var isSelectionGestureEnabled = true
-
+
public var selectionLimit: Int32 = 100
public var reachedSelectionLimit: ((Int32) -> Void)?
-
+
private var visibleTopInset: CGFloat?
private var originalTopInset: CGFloat?
-
+
public var passthroughPeerSelection = false
-
+
let hideArhiveIntro = ValuePromise(false, ignoreRepeated: true)
-
+
private let chatFolderUpdates = Promise()
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
private var updateIsMainTabDisposable: Disposable?
-
+
public var scrollHeightTopInset: CGFloat {
didSet {
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
}
}
-
+
public var startedScrollingAtUpperBound: Bool = false
-
+
private let autoSetReady: Bool
-
+
public let isMainTab = ValuePromise(false, ignoreRepeated: true)
-
+
public var synchronousDrawingWhenNotAnimated: Bool = false
-
+
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool, isMainTab: Bool?) {
self.context = context
self.location = location
@@ -1366,34 +1366,34 @@ public final class ChatListNode: ListViewImpl {
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.autoSetReady = autoSetReady
-
+
if let isMainTab {
self.isMainTab.set(isMainTab)
}
-
+
var isSelecting = false
if case .peers(_, true, _, _, _, _, _) = mode {
isSelecting = true
}
-
+
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), archiveStoryState: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
-
+
self.theme = theme
-
+
self.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight
-
+
super.init()
-
+
if case .internal = context.sharedContext.applicationBindings.appBuildType {
//self.useMainQueueTransactions = true
}
-
+
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
self.verticalScrollIndicatorFollowsOverscroll = true
-
+
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
-
+
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
activateSearch()
@@ -1537,7 +1537,7 @@ public final class ChatListNode: ListViewImpl {
guard case let .chatList(groupId) = strongSelf.location else {
return
}
-
+
let isPremium = peer?.isPremium ?? false
let location: TogglePeerChatPinnedLocation
if let chatListFilter = chatListFilter {
@@ -1745,7 +1745,7 @@ public final class ChatListNode: ListViewImpl {
guard let self else {
return
}
-
+
let activeSessionsContext = self.context.engine.privacy.activeSessions()
let _ = (activeSessionsContext.state
|> filter { state in
@@ -1756,7 +1756,7 @@ public final class ChatListNode: ListViewImpl {
guard let self else {
return
}
-
+
let recentSessionsController = self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext)
self.push?(recentSessionsController)
})
@@ -1769,10 +1769,10 @@ public final class ChatListNode: ListViewImpl {
guard let self else {
return
}
-
+
if isPositive {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
-
+
let animationBackgroundColor: UIColor
if presentationData.theme.overallDarkAppearance {
animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
@@ -1786,14 +1786,14 @@ public final class ChatListNode: ListViewImpl {
default:
break
}
-
+
return true
}))
-
+
let _ = self.context.engine.privacy.confirmNewSessionReview(id: newSessionReview.id).startStandalone()
} else {
self.push?(NewSessionInfoScreen(context: self.context, newSessionReview: newSessionReview))
-
+
#if DEBUG
#else
let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).startStandalone()
@@ -1803,7 +1803,7 @@ public final class ChatListNode: ListViewImpl {
guard let self else {
return
}
-
+
if isPositive {
let _ = self.context.engine.accountData.confirmBotConnectionReview(botId: newBotConnectionReview.botId).startStandalone()
} else {
@@ -1820,7 +1820,7 @@ public final class ChatListNode: ListViewImpl {
guard let self, let result else {
return
}
-
+
self.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(result), contents: result.chatFolderLinkContents))
})
}, hideChatFolderUpdates: { [weak self] in
@@ -1833,7 +1833,7 @@ public final class ChatListNode: ListViewImpl {
guard let self, let result else {
return
}
-
+
if let localFilterId = result.chatFolderLinkContents.localFilterId {
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).startStandalone()
}
@@ -1871,16 +1871,16 @@ public final class ChatListNode: ListViewImpl {
context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: self.context.sharedContext.mainWindow?.viewController as? NavigationController, dismissInput: {})
})
nodeInteraction.isInlineMode = isInlineMode
-
+
let viewProcessingQueue = self.viewProcessingQueue
-
+
let shouldLoadCanMessagePeer: Bool
if case .peers = mode {
shouldLoadCanMessagePeer = true
} else {
shouldLoadCanMessagePeer = false
}
-
+
let chatListViewUpdate = self.chatListLocation.get()
|> distinctUntilChanged
|> mapToSignal { listLocation -> Signal<(ChatListNodeViewUpdate, ChatListFilter?), NoError> in
@@ -1889,12 +1889,12 @@ public final class ChatListNode: ListViewImpl {
return (update, listLocation.filter)
}
}
-
+
let previousState = Atomic(value: self.currentState)
let previousView = Atomic(value: nil)
let previousHideArchivedFolderByDefault = Atomic(value: nil)
let currentRemovingItemId = self.currentRemovingItemId
-
+
let savedMessagesPeer: Signal
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))
@@ -1909,14 +1909,14 @@ public final class ChatListNode: ListViewImpl {
} else {
savedMessagesPeer = .single(nil)
}
-
+
let hideArchivedFolderByDefault = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.chatArchiveSettings))
|> map { view -> Bool in
let settings: ChatArchiveSettings = view?.get(ChatArchiveSettings.self) ?? .default
return settings.isHiddenByDefault
}
|> distinctUntilChanged
-
+
let displayArchiveIntro: Signal
if case .chatList(.archive) = location {
let displayArchiveIntroData = context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.archiveIntroDismissedKey())
@@ -1944,21 +1944,21 @@ public final class ChatListNode: ListViewImpl {
} else {
displayArchiveIntro = .single(false)
}
-
+
self.updateIsMainTabDisposable = (self.isMainTab.get()
|> deliverOnMainQueue).startStrict(next: { isMainTab in
if isMainTab {
let _ = context.engine.privacy.cleanupSessionReviews().startStandalone()
}
}).strict()
-
+
let storageInfo: Signal
if !"".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil {
let totalSizeSignal = combineLatest(context.account.postbox.mediaBox.storageBox.totalSize(), context.account.postbox.mediaBox.cacheStorageBox.totalSize())
|> map { a, b -> Int64 in
return a + b
}
-
+
storageInfo = totalSizeSignal
|> take(1)
|> mapToSignal { initialSize -> Signal in
@@ -1967,32 +1967,32 @@ public final class ChatListNode: ListViewImpl {
#else
let fractionLimit: Double = 0.3
#endif
-
+
let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
-
+
let initialFraction: Double
if deviceFreeSpace != 0 && initialSize != 0 {
initialFraction = Double(initialSize) / Double(deviceFreeSpace + initialSize)
} else {
initialFraction = 0.0
}
-
+
let initialReportSize: Double?
if initialFraction > fractionLimit {
initialReportSize = Double(initialSize)
} else {
initialReportSize = nil
}
-
+
final class ReportState {
var lastSize: Int64
-
+
init(lastSize: Int64) {
self.lastSize = lastSize
}
}
-
+
let state = Atomic(value: ReportState(lastSize: initialSize))
let updatedReportSize: Signal = Signal { subscriber in
let disposable = totalSizeSignal.start(next: { size in
@@ -2006,30 +2006,30 @@ public final class ChatListNode: ListViewImpl {
}
if updatedSize >= 0 {
let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
-
+
let updatedFraction: Double
if deviceFreeSpace != 0 && updatedSize != 0 {
updatedFraction = Double(updatedSize) / Double(deviceFreeSpace + updatedSize)
} else {
updatedFraction = 0.0
}
-
+
let updatedReportSize: Double?
if updatedFraction > fractionLimit {
updatedReportSize = Double(updatedSize)
} else {
updatedReportSize = nil
}
-
+
subscriber.putNext(updatedReportSize)
}
})
-
+
return ActionDisposable {
disposable.dispose()
}
}
-
+
return .single(initialReportSize)
|> then(
updatedReportSize
@@ -2038,9 +2038,9 @@ public final class ChatListNode: ListViewImpl {
} else {
storageInfo = .single(nil)
}
-
+
let currentPeerId: EnginePeer.Id = context.account.peerId
-
+
let contacts: Signal<[ChatListContactPeer], NoError>
if case .chatList(groupId: .root) = location, chatListFilter == nil, case .chatList = mode {
contacts = ApplicationSpecificNotice.displayChatListContacts(accountManager: context.sharedContext.accountManager)
@@ -2049,7 +2049,7 @@ public final class ChatListNode: ListViewImpl {
if value {
return .single([])
}
-
+
return context.engine.messages.chatList(group: .root, count: 10)
|> map { chatList -> Bool in
if chatList.items.count >= 5 {
@@ -2063,7 +2063,7 @@ public final class ChatListNode: ListViewImpl {
if hasChats {
return .single([])
}
-
+
return context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
)
@@ -2099,9 +2099,9 @@ public final class ChatListNode: ListViewImpl {
} else {
contacts = .single([])
}
-
+
let accountPeerId = context.account.peerId
-
+
let chatListFilters: Signal<[ChatListFilter]?, NoError>
if case .chatList = mode {
chatListFilters = combineLatest(queue: .mainQueue(),
@@ -2123,9 +2123,9 @@ public final class ChatListNode: ListViewImpl {
chatListFilters = .single(nil)
}
let previousChatListFilters = Atomic<[ChatListFilter]?>(value: nil)
-
+
let previousAccountIsPremium = Atomic(value: nil)
-
+
let accountIsPremium = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
@@ -2133,7 +2133,13 @@ public final class ChatListNode: ListViewImpl {
return peer?.isPremium ?? false
}
|> distinctUntilChanged
-
+
+ // WinterGram: a reactive snapshot of the hidden-archive peer ids so the chat list re-filters
+ // immediately when a chat is stashed/unstashed (rather than only on the next view update).
+ let winterGramStashedPeerIds = winterGramSettings(accountManager: context.sharedContext.accountManager)
+ |> map { Set($0.stashedPeerIds) }
+ |> distinctUntilChanged
+
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue,
hideArchivedFolderByDefault,
displayArchiveIntro,
@@ -2143,22 +2149,23 @@ public final class ChatListNode: ListViewImpl {
self.statePromise.get(),
contacts,
chatListFilters,
- accountIsPremium
+ accountIsPremium,
+ winterGramStashedPeerIds
)
- |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, savedMessagesPeer, updateAndFilter, state, contacts, chatListFilters, accountIsPremium) -> Signal in
+ |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, savedMessagesPeer, updateAndFilter, state, contacts, chatListFilters, accountIsPremium, winterGramStashedPeerIds) -> Signal in
let (update, filter) = updateAndFilter
-
+
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
-
+
let innerIsMainTab = location == .chatList(groupId: .root) && chatListFilter == nil
-
- let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: innerIsMainTab)
+
+ let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: innerIsMainTab, stashedPeerIds: winterGramStashedPeerIds)
var isEmpty = true
var entries = rawEntries.filter { entry in
switch entry {
case let .PeerEntry(peerEntry):
let peer = peerEntry.peer
-
+
switch mode {
case .chatList:
isEmpty = false
@@ -2168,12 +2175,12 @@ public final class ChatListNode: ListViewImpl {
guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false }
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false }
-
+
if let peer = peer.peer {
if peer.id.isRepliesOrVerificationCodes {
return false
}
-
+
switch peer {
case let .user(user):
if user.botInfo != nil {
@@ -2204,7 +2211,7 @@ public final class ChatListNode: ListViewImpl {
break
}
}
-
+
if filter.contains(.onlyGroupsAndChannels) {
if case .channel = peer.chatMainPeer {
} else if case .legacyGroup = peer.chatMainPeer {
@@ -2223,7 +2230,7 @@ public final class ChatListNode: ListViewImpl {
return false
}
}
-
+
if filter.contains(.onlyChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
} else {
@@ -2231,12 +2238,12 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
if filter.contains(.excludeChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
}
}
-
+
if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer) {
@@ -2246,7 +2253,7 @@ public final class ChatListNode: ListViewImpl {
return false
}
}
-
+
if filter.contains(.onlyManageable) && filter.contains(.excludeDisabled) {
if let peer = peer.peers[peer.peerId] {
var canManage = false
@@ -2262,7 +2269,7 @@ public final class ChatListNode: ListViewImpl {
} else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) {
canManage = true
}
-
+
if canManage {
} else {
return false
@@ -2271,7 +2278,7 @@ public final class ChatListNode: ListViewImpl {
return false
}
}
-
+
isEmpty = false
return true
case let .peerType(peerTypes, _):
@@ -2418,14 +2425,14 @@ public final class ChatListNode: ListViewImpl {
if isEmpty {
entries = [.HeaderEntry]
}
-
+
let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter)
let previousView = previousView.swap(processedView)
let previousState = previousState.swap(state)
-
+
let reason: ChatListNodeViewTransitionReason
var prepareOnMainQueue = false
-
+
var previousWasEmptyOrSingleHole = false
if let previous = previousView {
if previous.filteredEntries.count == 1 {
@@ -2440,9 +2447,9 @@ public final class ChatListNode: ListViewImpl {
} else {
previousWasEmptyOrSingleHole = true
}
-
+
var updatedScrollPosition = update.scrollPosition
-
+
if previousWasEmptyOrSingleHole {
reason = .initial
if previousView == nil {
@@ -2466,9 +2473,9 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
let removingItemId = currentRemovingItemId.with { $0 }
-
+
var disableAnimations = true
if previousState.editing != state.editing {
disableAnimations = false
@@ -2477,7 +2484,7 @@ public final class ChatListNode: ListViewImpl {
var updatedPinnedChats: [EnginePeer.Id] = []
var previousPinnedThreads: [Int64] = []
var updatedPinnedThreads: [Int64] = []
-
+
var didIncludeRemovingPeerId = false
var didIncludeHiddenByDefaultArchive = false
var didIncludeHiddenThread = false
@@ -2486,7 +2493,7 @@ public final class ChatListNode: ListViewImpl {
if case let .PeerEntry(peerEntry) = entry {
let index = peerEntry.index
let threadInfo = peerEntry.threadInfo
-
+
if let threadInfo, threadInfo.isHidden {
didIncludeHiddenThread = true
}
@@ -2513,13 +2520,13 @@ public final class ChatListNode: ListViewImpl {
var doesIncludeRemovingPeerId = false
var doesIncludeArchive = false
var doesIncludeHiddenByDefaultArchive = false
-
+
var doesIncludeHiddenThread = false
for entry in processedView.filteredEntries {
if case let .PeerEntry(peerEntry) = entry {
let index = peerEntry.index
let threadInfo = peerEntry.threadInfo
-
+
if let threadInfo, threadInfo.isHidden {
doesIncludeHiddenThread = true
}
@@ -2530,7 +2537,7 @@ public final class ChatListNode: ListViewImpl {
updatedPinnedThreads.append(threadId)
}
}
-
+
if case let .chatList(index) = index, ChatListNodeState.ItemId(peerId: index.messageIndex.id.peerId, threadId: nil) == removingItemId {
doesIncludeRemovingPeerId = true
} else if case let .forum(_, _, threadId, _, _) = index {
@@ -2568,21 +2575,21 @@ public final class ChatListNode: ListViewImpl {
disableAnimations = false
}
}
-
+
if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault {
disableAnimations = false
}
-
+
var searchMode = false
if case .peers = mode {
searchMode = true
}
-
+
if filter != previousView?.filter {
disableAnimations = true
updatedScrollPosition = nil
}
-
+
let filterData = filter.flatMap { filter -> ChatListItemFilterData? in
if case let .filter(_, _, _, data) = filter {
return ChatListItemFilterData(excludesArchived: data.excludeArchived)
@@ -2590,7 +2597,7 @@ public final class ChatListNode: ListViewImpl {
return nil
}
}
-
+
var forceAllUpdated = false
let previousChatListFiltersValue = previousChatListFilters.swap(chatListFilters)
if chatListFilters != previousChatListFiltersValue {
@@ -2600,19 +2607,19 @@ public final class ChatListNode: ListViewImpl {
forceAllUpdated = true
}
let presentationData = state.presentationData
-
+
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated)
|> 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)
}
-
+
let appliedTransition = chatListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in
if let strongSelf = self {
return strongSelf.enqueueTransition(transition)
}
return .complete()
}
-
+
self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
if let strongSelf = self, let chatListView = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView {
let originalList = chatListView.originalList
@@ -2623,14 +2630,14 @@ public final class ChatListNode: ListViewImpl {
} else if range.firstIndex >= 5, range.lastIndex >= originalList.items.count - 5, originalList.hasEarlier, let firstItem = originalList.items.first {
location = .navigation(index: firstItem.index, filter: strongSelf.chatListFilter)
}
-
+
if let location = location, location != strongSelf.currentLocation {
strongSelf.setChatListLocation(location)
}
-
+
strongSelf.enqueueHistoryPreloadUpdate()
}
-
+
var refreshStoryPeerIds: [EnginePeer.Id] = []
var isHiddenItemVisible = false
if let range = range.visibleRange {
@@ -2643,15 +2650,15 @@ public final class ChatListNode: ListViewImpl {
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(peerEntry):
let threadInfo = peerEntry.threadInfo
-
+
if let threadInfo, threadInfo.isHidden {
isHiddenItemVisible = true
}
-
+
if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer {
refreshStoryPeerIds.append(user.id)
}
-
+
break
case .GroupReferenceEntry:
isHiddenItemVisible = true
@@ -2672,11 +2679,11 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
self.interaction = nodeInteraction
-
+
self.chatListDisposable.set(appliedTransition.startStrict())
-
+
let initialLocation: ChatListNodeLocation
switch mode {
case .chatList:
@@ -2685,7 +2692,7 @@ public final class ChatListNode: ListViewImpl {
initialLocation = .initial(count: 200, filter: self.chatListFilter)
}
self.setChatListLocation(initialLocation)
-
+
let engine = context.engine
let previousPeerCache = Atomic<[EnginePeer.Id: EnginePeer]>(value: [:])
let previousActivities = Atomic(value: nil)
@@ -2704,7 +2711,7 @@ public final class ChatListNode: ListViewImpl {
}
})
}
-
+
var foundAllPeers = true
var cachedResult: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = [:]
previousPeerCache.with { dict -> Void in
@@ -2778,16 +2785,16 @@ public final class ChatListNode: ListViewImpl {
case .savedMessagesChats:
return [:]
}
-
+
var chatResult: [(EnginePeer, PeerInputActivity)] = []
-
+
for (peerId, activity) in activities {
if let maybePeer = peerMap[peerId], let peer = maybePeer {
chatResult.append((peer, activity))
peerCache[peerId] = peer
}
}
-
+
result[itemId] = chatResult
}
let _ = previousPeerCache.swap(peerCache)
@@ -2845,7 +2852,7 @@ public final class ChatListNode: ListViewImpl {
}
}
})
-
+
self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in
guard let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries else {
return .single(false)
@@ -2853,19 +2860,19 @@ public final class ChatListNode: ListViewImpl {
guard fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count else {
return .single(false)
}
-
+
switch strongSelf.location {
case let .chatList(groupId):
let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex]
let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex]
-
+
var referenceId: EngineChatList.PinnedItem.Id?
var beforeAll = false
switch toEntry {
case let .PeerEntry(peerEntry):
let index = peerEntry.index
let promoInfo = peerEntry.promoInfo
-
+
if promoInfo != nil {
beforeAll = true
} else {
@@ -2876,7 +2883,7 @@ public final class ChatListNode: ListViewImpl {
default:
break
}
-
+
if case let .index(index) = fromEntry.sortIndex, case let .chatList(chatListIndex) = index, let _ = chatListIndex.pinningIndex {
let location: TogglePeerChatPinnedLocation
if let chatListFilter = chatListFilter {
@@ -2884,12 +2891,12 @@ public final class ChatListNode: ListViewImpl {
} else {
location = .group(groupId._asGroup())
}
-
+
let engine = strongSelf.context.engine
return engine.peers.getPinnedItemIds(location: location)
|> mapToSignal { itemIds -> Signal in
var itemIds = itemIds
-
+
var itemId: EngineChatList.PinnedItem.Id?
switch fromEntry {
case let .PeerEntry(peerEntry):
@@ -2899,7 +2906,7 @@ public final class ChatListNode: ListViewImpl {
default:
break
}
-
+
if let itemId = itemId {
itemIds = itemIds.filter({ $0 != itemId })
if let referenceId = referenceId {
@@ -2934,7 +2941,7 @@ public final class ChatListNode: ListViewImpl {
case let .forum(peerId):
let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex]
let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex]
-
+
var referenceId: Int64?
var beforeAll = false
switch toEntry {
@@ -2949,13 +2956,13 @@ public final class ChatListNode: ListViewImpl {
default:
break
}
-
+
if case let .index(index) = fromEntry.sortIndex, case let .forum(pinningIndex, _, _, _, _) = index, case .index = pinningIndex {
let engine = strongSelf.context.engine
return engine.peers.getForumChannelPinnedTopics(id: peerId)
|> mapToSignal { itemIds -> Signal in
var itemIds = itemIds
-
+
var itemId: Int64?
switch fromEntry {
case let .PeerEntry(peerEntry):
@@ -2965,7 +2972,7 @@ public final class ChatListNode: ListViewImpl {
default:
break
}
-
+
if let itemId = itemId {
itemIds = itemIds.filter({ $0 != itemId })
if let referenceId = referenceId {
@@ -3007,7 +3014,7 @@ public final class ChatListNode: ListViewImpl {
return .single(false)
}
}
-
+
self.beganInteractiveDragging = { [weak self] _ in
guard let strongSelf = self else {
return
@@ -3018,7 +3025,7 @@ public final class ChatListNode: ListViewImpl {
case let .known(value):
strongSelf.startedScrollingAtUpperBound = value <= 0.001
}
-
+
if strongSelf.currentState.peerIdWithRevealedOptions != nil {
strongSelf.updateState { state in
var state = state
@@ -3026,10 +3033,10 @@ public final class ChatListNode: ListViewImpl {
return state
}
}
-
+
strongSelf.didBeginInteractiveDragging?(strongSelf)
}
-
+
self.didEndScrolling = { [weak self] _ in
guard let strongSelf = self else {
return
@@ -3050,7 +3057,7 @@ public final class ChatListNode: ListViewImpl {
}*/
}
}
-
+
self.scrollToTopOptionPromise.set(combineLatest(
renderedTotalUnreadCount(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine) |> deliverOnMainQueue,
self.scrolledAtTop.get()
@@ -3061,7 +3068,7 @@ public final class ChatListNode: ListViewImpl {
return .top
}
})
-
+
self.visibleContentOffsetChanged = { [weak self] offset, transition in
guard let strongSelf = self else {
return
@@ -3077,7 +3084,7 @@ public final class ChatListNode: ListViewImpl {
atTop = value <= -strongSelf.tempTopInset
}
strongSelf.scrolledAtTopValue = atTop
-
+
var maxPinnedOffset: CGFloat = 0.0
strongSelf.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item {
@@ -3098,24 +3105,24 @@ public final class ChatListNode: ListViewImpl {
pinnedScrollFraction = maxPinnedOffset / strongSelf.insets.top
}
}
-
+
strongSelf.contentOffsetChanged?(offset)
strongSelf.pinnedScrollFraction = pinnedScrollFraction
strongSelf.pinnedHeaderDisplayFractionUpdated?(transition)
}
-
+
self.dynamicVisualInsets = { [weak self] in
guard let self else {
return UIEdgeInsets()
}
-
+
let _ = self
return UIEdgeInsets()
}
-
+
self.pollFilterUpdates()
self.resetFilter()
-
+
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
selectionRecognizer.shouldBegin = { [weak self] in
guard let strongSelf = self else {
@@ -3125,7 +3132,7 @@ public final class ChatListNode: ListViewImpl {
}
self.view.addGestureRecognizer(selectionRecognizer)
}
-
+
deinit {
self.chatListDisposable.dispose()
self.activityStatusesDisposable?.dispose()
@@ -3134,14 +3141,14 @@ public final class ChatListNode: ListViewImpl {
self.chatFilterUpdatesDisposable?.dispose()
self.updateIsMainTabDisposable?.dispose()
}
-
+
func updateFilter(_ filter: ChatListFilter?) {
if filter?.id != self.chatListFilter?.id {
self.chatListFilter = filter
self.resetFilter()
}
}
-
+
func hasItemsToBeRevealed() -> Bool {
if self.currentState.hiddenItemShouldBeTemporaryRevealed {
return false
@@ -3161,10 +3168,10 @@ public final class ChatListNode: ListViewImpl {
}
}
})
-
+
return isHiddenItemVisible
}
-
+
func revealScrollHiddenItem() {
var isHiddenItemVisible = false
self.forEachItemNode({ itemNode in
@@ -3193,10 +3200,10 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
private func pollFilterUpdates() {
self.chatFolderUpdates.set(.single(nil))
-
+
/*guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else {
self.chatFolderUpdates.set(.single(nil))
return
@@ -3210,7 +3217,7 @@ public final class ChatListNode: ListViewImpl {
self.chatFolderUpdates.set(.single(result))
})*/
}
-
+
private func resetFilter() {
if let chatListFilter = self.chatListFilter, chatListFilter.id != Int32.max {
self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters()
@@ -3234,7 +3241,7 @@ public final class ChatListNode: ListViewImpl {
self.updatedFilterDisposable.set(nil)
}
}
-
+
public func updateThemeAndStrings(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) {
if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || dateTimeFormat != self.currentState.presentationData.dateTimeFormat {
self.theme = theme
@@ -3242,7 +3249,7 @@ public final class ChatListNode: ListViewImpl {
self.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: theme.chatList.pinnedItemBackgroundColor, direction: true)
}
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
-
+
self.updateState { state in
var state = state
state.presentationData = ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations)
@@ -3250,7 +3257,7 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
public func updateState(_ f: (ChatListNodeState) -> ChatListNodeState) {
let state = f(self.currentState)
if state != self.currentState {
@@ -3259,18 +3266,18 @@ public final class ChatListNode: ListViewImpl {
self.selectionCountChanged?(state.selectedPeerIds.count)
}
}
-
+
private func enqueueTransition(_ transition: ChatListNodeListViewTransition) -> Signal {
return Signal { [weak self] subscriber in
if let strongSelf = self {
if let _ = strongSelf.enqueuedTransition {
preconditionFailure()
}
-
+
strongSelf.enqueuedTransition = (transition, {
subscriber.putCompletion()
})
-
+
if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout {
strongSelf.dequeueTransition()
} else {
@@ -3282,19 +3289,19 @@ public final class ChatListNode: ListViewImpl {
} else {
subscriber.putCompletion()
}
-
+
return EmptyDisposable
} |> runOn(Queue.mainQueue())
}
-
+
private func dequeueTransition() {
if let (transition, completion) = self.enqueuedTransition {
self.enqueuedTransition = nil
-
+
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.chatListView = transition.chatListView
-
+
if strongSelf.fillPreloadItems {
let filteredEntries = transition.chatListView.filteredEntries
var preloadItems: [ChatHistoryPreloadItem] = []
@@ -3324,7 +3331,7 @@ public final class ChatListNode: ListViewImpl {
}
strongSelf.preloadItems.set(.single(preloadItems))
}
-
+
var pinnedOverscroll = false
if case .chatList = strongSelf.mode {
let entryCount = transition.chatListView.filteredEntries.count
@@ -3352,7 +3359,7 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
if pinnedOverscroll != (strongSelf.keepTopItemOverscrollBackground != nil) {
if pinnedOverscroll {
strongSelf.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: strongSelf.theme.chatList.pinnedItemBackgroundColor, direction: true)
@@ -3360,13 +3367,13 @@ public final class ChatListNode: ListViewImpl {
strongSelf.keepTopItemOverscrollBackground = nil
}
}
-
+
if let scrollToItem = transition.scrollToItem, case .center = scrollToItem.position {
if let itemNode = strongSelf.itemNodeAtIndex(scrollToItem.index) as? ChatListItemNode {
itemNode.flashHighlight()
}
}
-
+
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
@@ -3375,7 +3382,7 @@ public final class ChatListNode: ListViewImpl {
strongSelf.didSetContentsReady = true
strongSelf._contentsReady.set(true)
}
-
+
var isEmpty = false
var isLoading = false
var hasArchiveInfo = false
@@ -3421,7 +3428,7 @@ public final class ChatListNode: ListViewImpl {
}
}
}
-
+
let isEmptyState: ChatListNodeEmptyState
if transition.chatListView.isLoading {
isEmptyState = .empty(isLoading: true, hasArchiveInfo: hasArchiveInfo)
@@ -3459,7 +3466,7 @@ public final class ChatListNode: ListViewImpl {
}
isEmptyState = .notEmpty(containsChats: containsChats || hasArchive, onlyArchive: hasArchive && !containsChats, onlyGeneralThread: hasGeneral && threadCount == 1)
}
-
+
var insertedPeerIds: [EnginePeer.Id] = []
for item in transition.insertItems {
if let item = item.item as? ChatListItem {
@@ -3476,26 +3483,26 @@ public final class ChatListNode: ListViewImpl {
if !insertedPeerIds.isEmpty {
strongSelf.addedVisibleChatsWithPeerIds?(insertedPeerIds)
}
-
+
var isEmptyUpdate: ContainedViewLayoutTransition = .immediate
if transition.options.contains(.AnimateInsertion) || transition.animateCrossfade {
isEmptyUpdate = .animated(duration: 0.25, curve: .easeInOut)
}
-
+
if strongSelf.currentIsEmptyState != isEmptyState {
strongSelf.currentIsEmptyState = isEmptyState
strongSelf.isEmptyUpdated?(isEmptyState, transition.chatListView.filter != nil, isEmptyUpdate)
}
-
+
if !strongSelf.hasUpdatedAppliedChatListFilterValueOnce || transition.chatListView.filter != strongSelf.currentAppliedChatListFilterValue {
strongSelf.currentAppliedChatListFilterValue = transition.chatListView.filter
strongSelf.appliedChatListFilterValue.set(.single(transition.chatListView.filter))
}
-
+
completion()
}
}
-
+
var options = transition.options
//options.insert(.Synchronous)
if self.view.window != nil || self.synchronousDrawingWhenNotAnimated {
@@ -3507,7 +3514,7 @@ public final class ChatListNode: ListViewImpl {
options.insert(.PreferSynchronousDrawing)
}
}
-
+
var scrollToItem = transition.scrollToItem
if transition.adjustScrollToFirstItem {
var offset: CGFloat = 0.0
@@ -3523,12 +3530,12 @@ public final class ChatListNode: ListViewImpl {
}
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
-
+
let updatedOpaqueState: Any? = ChatListOpaqueTransactionState(chatListView: transition.chatListView)
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: updatedOpaqueState, completion: completion)
}
}
-
+
var isNavigationHidden: Bool {
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
@@ -3539,7 +3546,7 @@ public final class ChatListNode: ListViewImpl {
return true
}
}
-
+
var isNavigationInAFinalState: Bool {
switch self.visibleContentOffset() {
case let .known(value):
@@ -3559,7 +3566,7 @@ public final class ChatListNode: ListViewImpl {
return true
}
}
-
+
func adjustScrollOffsetForNavigation(isNavigationHidden: Bool) {
if self.isNavigationHidden == isNavigationHidden {
return
@@ -3579,20 +3586,20 @@ public final class ChatListNode: ListViewImpl {
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
-
+
public func fixContentOffset(offset: CGFloat) {
let _ = self.scrollToOffsetFromTop(offset, animated: false)
-
+
/*let scrollToItem: ListViewScrollToItem = ListViewScrollToItem(index: 0, position: .top(-offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })*/
}
-
+
public var ignoreStoryInsetAdjustment: Bool = false
private var previousStoriesInset: CGFloat?
-
+
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, visibleTopInset: CGFloat, originalTopInset: CGFloat, storiesInset: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat) {
//print("inset: \(updateSizeAndInsets.insets.top)")
-
+
var highlightedLocation: ChatListHighlightedLocation?
if case let .forum(peerId) = inlineNavigationLocation {
highlightedLocation = ChatListHighlightedLocation(location: .peer(id: peerId), progress: inlineNavigationTransitionFraction)
@@ -3601,13 +3608,13 @@ public final class ChatListNode: ListViewImpl {
if (self.interaction?.inlineNavigationLocation == nil) != (highlightedLocation == nil) {
navigationLocationPresenceUpdated = true
}
-
+
var navigationLocationUpdated = false
if self.interaction?.inlineNavigationLocation != highlightedLocation {
self.interaction?.inlineNavigationLocation = highlightedLocation
navigationLocationUpdated = true
}
-
+
let insetDelta: CGFloat = 0.0
if navigationLocationPresenceUpdated {
let targetTopInset: CGFloat
@@ -3617,17 +3624,17 @@ public final class ChatListNode: ListViewImpl {
targetTopInset = self.originalTopInset ?? self.insets.top
}
let immediateInsetDelta = self.insets.top - targetTopInset
-
+
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, additionalScrollDistance: immediateInsetDelta, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: self.visibleSize, insets: UIEdgeInsets(top: targetTopInset, left: self.insets.left, bottom: self.insets.bottom, right: self.insets.right), duration: 0.0, curve: .Default(duration: 0.0)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
-
+
self.visualInsets = UIEdgeInsets(top: visibleTopInset, left: 0.0, bottom: 0.0, right: 0.0)
-
+
self.visibleTopInset = visibleTopInset
self.originalTopInset = originalTopInset
-
+
var additionalScrollDistance: CGFloat = 0.0
-
+
if let previousStoriesInset = self.previousStoriesInset {
if self.ignoreStoryInsetAdjustment {
//additionalScrollDistance += -20.0
@@ -3644,28 +3651,28 @@ public final class ChatListNode: ListViewImpl {
}
self.previousStoriesInset = storiesInset
//print("storiesInset: \(storiesInset), additionalScrollDistance: \(additionalScrollDistance)")
-
+
var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency]
if navigationLocationUpdated {
options.insert(.ForceUpdate)
-
+
if transition.isAnimated {
options.insert(.AnimateInsertion)
}
-
+
additionalScrollDistance += insetDelta
}
self.ignoreStopScrolling = true
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: options, scrollToItem: nil, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.ignoreStopScrolling = false
-
+
if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true
-
+
self.dequeueTransition()
}
}
-
+
public func scrollToPosition(_ position: ChatListNodeScrollPosition, animated: Bool = true) {
var additionalDelta: CGFloat = 0.0
switch position {
@@ -3675,7 +3682,7 @@ public final class ChatListNode: ListViewImpl {
self.tempTopInset = ChatListNavigationBar.storiesScrollHeight
}
}
-
+
if let list = self.chatListView?.originalList {
if !list.hasLater {
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(additionalDelta), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
@@ -3688,17 +3695,17 @@ public final class ChatListNode: ListViewImpl {
self.setChatListLocation(location)
}
}
-
+
private func setChatListLocation(_ location: ChatListNodeLocation) {
self.currentLocation = location
self.chatListLocation.set(location)
}
-
+
private func relativeUnreadChatListIndex(position: EngineChatList.RelativePosition) -> Signal {
guard case let .chatList(groupId) = self.location else {
return .single(nil)
}
-
+
let engine = self.context.engine
return self.context.sharedContext.accountManager.transaction { transaction -> Signal in
var filter = true
@@ -3712,25 +3719,25 @@ public final class ChatListNode: ListViewImpl {
}
|> switchToLatest
}
-
+
public func selectChat(_ option: ChatListSelectionOption) {
guard let interaction = self.interaction else {
return
}
-
+
guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else {
return
}
-
+
guard let range = self.displayedItemRange.loadedRange else {
return
}
-
+
let entryCount = chatListView.filteredEntries.count
var current: (EngineChatList.Item.Index, EnginePeer, Int)? = nil
var previous: (EngineChatList.Item.Index, EnginePeer)? = nil
var next: (EngineChatList.Item.Index, EnginePeer)? = nil
-
+
outer: for i in range.firstIndex ..< range.lastIndex {
if i < 0 || i >= entryCount {
assertionFailure()
@@ -3746,7 +3753,7 @@ public final class ChatListNode: ListViewImpl {
break
}
}
-
+
switch option {
case .previous(unread: true), .next(unread: true):
let position: EngineChatList.RelativePosition
@@ -3826,14 +3833,14 @@ public final class ChatListNode: ListViewImpl {
guard case let .chatList(groupId) = self.location else {
return
}
-
+
let shouldLoadCanMessagePeer: Bool
if case .peers = self.mode {
shouldLoadCanMessagePeer = true
} else {
shouldLoadCanMessagePeer = false
}
-
+
let _ = (chatListViewForLocation(chatListLocation: .chatList(groupId: groupId), location: .initial(count: 10, filter: filter), account: self.context.account, shouldLoadCanMessagePeer: shouldLoadCanMessagePeer)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { update in
@@ -3848,28 +3855,28 @@ public final class ChatListNode: ListViewImpl {
})
}
}
-
+
private func enqueueHistoryPreloadUpdate() {
}
-
+
public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
guard let interaction = self.interaction else {
return
}
-
+
if let chatLocation = chatLocation {
interaction.highlightedChatLocation = ChatListHighlightedLocation(location: chatLocation, progress: progress)
} else {
interaction.highlightedChatLocation = nil
}
-
+
self.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListItemNode {
itemNode.updateIsHighlighted(transition: transition)
}
}
}
-
+
private func currentlyVisibleLatestChatListIndex() -> EngineChatList.Item.Index? {
guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else {
return nil
@@ -3891,7 +3898,7 @@ public final class ChatListNode: ListViewImpl {
}
return nil
}
-
+
private func peerAtPoint(_ point: CGPoint) -> EnginePeer? {
var resultPeer: EnginePeer?
self.forEachVisibleItemNode { itemNode in
@@ -3908,7 +3915,7 @@ public final class ChatListNode: ListViewImpl {
}
return resultPeer
}
-
+
private func threadIdAtPoint(_ point: CGPoint) -> Int64? {
var resultThreadId: Int64?
self.forEachVisibleItemNode { itemNode in
@@ -3925,14 +3932,14 @@ public final class ChatListNode: ListViewImpl {
}
return resultThreadId
}
-
+
private var selectionPanState: (selecting: Bool, initialPeerId: EnginePeer.Id, toggledPeerIds: [[EnginePeer.Id]])?
private var threadSelectionPanState: (selecting: Bool, initialThreadId: Int64, toggledThreadIds: [[Int64]])?
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
private var selectionScrollDelta: CGFloat?
private var selectionLastLocation: CGPoint?
-
+
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
let location = recognizer.location(in: self.view)
switch recognizer.state {
@@ -3971,7 +3978,7 @@ public final class ChatListNode: ListViewImpl {
fatalError()
}
}
-
+
private func handlePanSelection(location: CGPoint) {
var location = location
if location.y < self.insets.top {
@@ -3979,7 +3986,7 @@ public final class ChatListNode: ListViewImpl {
} else if location.y > self.frame.height - self.insets.bottom {
location.y = self.frame.height - self.insets.bottom - 5.0
}
-
+
var hasState = false
switch self.location {
case .chatList:
@@ -3999,14 +4006,14 @@ public final class ChatListNode: ListViewImpl {
if peerId == peer.id {
previouslyToggled = true
updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1))
-
+
let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 }
self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting)
break
}
}
}
-
+
if !previouslyToggled {
updatedToggledPeerIds = state.toggledPeerIds
let isSelected = self.currentState.selectedPeerIds.contains(peer.id)
@@ -4015,7 +4022,7 @@ public final class ChatListNode: ListViewImpl {
self.interaction?.togglePeersSelection([.peer(peer)], state.selecting)
}
}
-
+
self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds)
}
}
@@ -4037,14 +4044,14 @@ public final class ChatListNode: ListViewImpl {
if toggledThreadId == threadId {
previouslyToggled = true
updatedToggledThreadIds = Array(state.toggledThreadIds.prefix(i + 1))
-
+
let threadIdsToToggle = Array(state.toggledThreadIds.suffix(state.toggledThreadIds.count - i - 1)).flatMap { $0 }
self.interaction?.toggleThreadsSelection(threadIdsToToggle.compactMap { $0 }, !state.selecting)
break
}
}
}
-
+
if !previouslyToggled {
updatedToggledThreadIds = state.toggledThreadIds
let isSelected = self.currentState.selectedThreadIds.contains(threadId)
@@ -4053,7 +4060,7 @@ public final class ChatListNode: ListViewImpl {
self.interaction?.toggleThreadsSelection([threadId], state.selecting)
}
}
-
+
self.threadSelectionPanState = (state.selecting, state.initialThreadId, updatedToggledThreadIds)
}
}
@@ -4089,7 +4096,7 @@ public final class ChatListNode: ListViewImpl {
self.selectionScrollActivationTimer = nil
}
}
-
+
private var selectionScrollSkipUpdate = false
private func setupSelectionScrolling() {
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
@@ -4098,7 +4105,7 @@ public final class ChatListNode: ListViewImpl {
let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta))
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
let _ = strongSelf.scrollWithDirection(direction, distance: distance)
-
+
if let location = strongSelf.selectionLastLocation {
if !strongSelf.selectionScrollSkipUpdate {
strongSelf.handlePanSelection(location: location)
@@ -4115,7 +4122,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if accountPeerId == peer.id {
return nil
}
-
+
if displayAutoremoveTimeout {
if let autoremoveTimeout = autoremoveTimeout {
return (NSAttributedString(string: strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string), false, true, .autoremove)
@@ -4123,7 +4130,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
return (NSAttributedString(string: strings.ChatList_LabelAutodeleteDisabled), false, false, .autoremove)
}
}
-
+
if let chatListFilters = chatListFilters {
let result = NSMutableAttributedString(string: "")
for case let .filter(_, title, _, data) in chatListFilters {
@@ -4135,14 +4142,14 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
result.append(title.rawAttributedString)
}
}
-
+
if result.length == 0 {
return nil
} else {
return (result, true, false, nil)
}
}
-
+
if peer.id.isReplies || peer.id.isVerificationCodes {
return nil
} else if case let .user(user) = peer {
@@ -4175,28 +4182,28 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
private let selectionGestureActivationThreshold: CGFloat = 5.0
-
+
var recognized: Bool? = nil
var initialLocation: CGPoint = CGPoint()
-
+
public var shouldBegin: (() -> Bool)?
-
+
public override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
-
+
self.minimumNumberOfTouches = 2
self.maximumNumberOfTouches = 2
}
-
+
public override func reset() {
super.reset()
-
+
self.recognized = nil
}
-
+
public override func touchesBegan(_ touches: Set, with event: UIEvent) {
super.touchesBegan(touches, with: event)
-
+
if let shouldBegin = self.shouldBegin, !shouldBegin() {
self.state = .failed
} else {
@@ -4204,17 +4211,17 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
self.initialLocation = touch.location(in: self.view)
}
}
-
+
public override func touchesMoved(_ touches: Set, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y)
-
+
let touchesArray = Array(touches)
if self.recognized == nil, touchesArray.count == 2 {
if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last {
let firstLocation = firstTouch.location(in: self.view)
let secondLocation = secondTouch.location(in: self.view)
-
+
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
let dx = v1.x - v2.x
let dy = v1.y - v2.y
@@ -4228,7 +4235,7 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
self.recognized = true
}
}
-
+
if let recognized = self.recognized, recognized {
super.touchesMoved(touches, with: event)
}
@@ -4253,7 +4260,7 @@ func chatListItemTags(location: ChatListControllerLocation, accountPeerId: Engin
guard let peer else {
return []
}
-
+
var result: [ChatListItemContent.Tag] = []
for case let .filter(id, title, _, data) in chatListFilters {
if data.color != nil {
diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift
index f9c4efa872..91d873dba9 100644
--- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift
+++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift
@@ -2,6 +2,7 @@ import Foundation
import UIKit
import TelegramCore
import TelegramPresentationData
+import TelegramUIPreferences
import MergeLists
import AccountContext
@@ -26,7 +27,7 @@ enum ChatListNodeEntrySortIndex: Comparable {
case sectionHeader
case contact(id: EnginePeer.Id, presence: EnginePeer.Presence)
case topPeer(Int)
-
+
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
switch lhs {
case let .index(lhsIndex):
@@ -122,7 +123,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
var storyState: ChatListNodeState.StoryState?
var requiresPremiumForMessaging: Bool
var displayAsTopicList: Bool
-
+
init(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
@@ -180,7 +181,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.displayAsTopicList = displayAsTopicList
}
-
+
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
if lhs.index != rhs.index {
return false
@@ -306,18 +307,18 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return true
}
}
-
+
struct ContactEntryData: Equatable {
var presentationData: ChatListPresentationData
var peer: EnginePeer
var presence: EnginePeer.Presence
-
+
init(presentationData: ChatListPresentationData, peer: EnginePeer, presence: EnginePeer.Presence) {
self.presentationData = presentationData
self.peer = peer
self.presence = presence
}
-
+
static func ==(lhs: ContactEntryData, rhs: ContactEntryData) -> Bool {
if lhs.presentationData !== rhs.presentationData {
return false
@@ -331,7 +332,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return true
}
}
-
+
struct GroupReferenceEntryData: Equatable {
var index: EngineChatList.Item.Index
var presentationData: ChatListPresentationData
@@ -344,7 +345,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
var hiddenByDefault: Bool
var appearsPinned: Bool
var storyState: ChatListNodeState.StoryState?
-
+
init(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
@@ -370,7 +371,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
self.appearsPinned = appearsPinned
self.storyState = storyState
}
-
+
static func ==(lhs: GroupReferenceEntryData, rhs: GroupReferenceEntryData) -> Bool {
if lhs.index != rhs.index {
return false
@@ -405,11 +406,11 @@ enum ChatListNodeEntry: Comparable, Identifiable {
if lhs.storyState != rhs.storyState {
return false
}
-
+
return true
}
}
-
+
case HeaderEntry
case PeerEntry(PeerEntryData)
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
@@ -420,7 +421,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
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 {
case .HeaderEntry:
@@ -445,7 +446,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return .topPeer(index)
}
}
-
+
var stableId: ChatListNodeEntryId {
switch self {
case .HeaderEntry:
@@ -475,11 +476,11 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return .TopPeer(peer.id)
}
}
-
+
static func <(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
-
+
static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
switch lhs {
case .HeaderEntry:
@@ -591,14 +592,14 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1
struct ChatListContactPeer {
var peer: EnginePeer
var presence: EnginePeer.Presence
-
+
init(peer: EnginePeer, presence: EnginePeer.Presence) {
self.peer = peer
self.presence = presence
}
}
-func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool) -> (entries: [ChatListNodeEntry], loading: Bool) {
+func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool, stashedPeerIds: Set) -> (entries: [ChatListNodeEntry], loading: Bool) {
var groupItems = view.groupItems
if isMainTab && state.archiveStoryState != nil && groupItems.isEmpty {
groupItems.append(EngineChatList.GroupItem(
@@ -608,16 +609,16 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
unreadCount: 0
))
}
-
+
var result: [ChatListNodeEntry] = []
-
+
var hasContacts = false
if !view.hasEarlier {
var existingPeerIds = Set()
for item in view.items {
existingPeerIds.insert(item.renderedPeer.peerId)
}
-
+
for contact in contacts {
if existingPeerIds.contains(contact.peer.id) {
continue
@@ -633,9 +634,9 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty))
}
}
-
+
var pinnedIndexOffset: UInt16 = 0
-
+
if !view.hasLater, case .chatList = mode {
var groupEntryCount = 0
for _ in groupItems {
@@ -643,24 +644,24 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
}
pinnedIndexOffset += UInt16(groupEntryCount)
}
-
+
let filteredAdditionalItemEntries = view.additionalItems.filter { item -> Bool in
return item.item.renderedPeer.peerId != state.hiddenPsaPeerId
}
-
+
var foundPeerIds = Set()
for peer in foundPeers {
foundPeerIds.insert(peer.0.id)
}
-
+
if !view.hasLater && savedMessagesPeer == nil {
pinnedIndexOffset += UInt16(filteredAdditionalItemEntries.count)
}
-
+
var hiddenGeneralThread: ChatListNodeEntry?
-
+
var hasPinned = false
-
+
loop: for entry in view.items {
var peerId: EnginePeer.Id?
var threadId: Int64?
@@ -673,13 +674,16 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
activityItemId = ChatListNodePeerInputActivities.ItemId(peerId: peerIdValue, threadId: threadIdValue)
threadId = threadIdValue
}
-
+
if let savedMessagesPeer = savedMessagesPeer, let peerId = peerId, savedMessagesPeer.id == peerId || foundPeerIds.contains(peerId) {
continue loop
}
if let peerId = peerId, state.pendingRemovalItemIds.contains(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) {
continue loop
}
+ if let peerId = peerId, stashedPeerIds.contains(peerId.toInt64()) {
+ continue loop
+ }
var updatedMessages = entry.messages
var updatedCombinedReadState = entry.readCounters
if let peerId = peerId, state.pendingClearHistoryPeerIds.contains(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) {
@@ -691,7 +695,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
if let draft = entry.draft {
draftState = ChatListItemContent.DraftState(draft: draft)
}
-
+
var hasActiveRevealControls = false
if let peerId {
hasActiveRevealControls = ChatListNodeState.ItemId(peerId: peerId, threadId: threadId) == state.peerIdWithRevealedOptions
@@ -700,19 +704,19 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
if let activityItemId {
inputActivities = state.peerInputActivities?.activities[activityItemId]
}
-
+
var isSelected = false
if let threadId, threadId != 0 {
isSelected = state.selectedThreadIds.contains(threadId)
} else if let peerId {
isSelected = state.selectedPeerIds.contains(peerId)
}
-
+
var threadInfo: ChatListItemContent.ThreadInfo?
if let threadData = entry.threadData, let threadId {
threadInfo = ChatListItemContent.ThreadInfo(id: threadId, info: threadData.info, isOwnedByMe: threadData.isOwnedByMe, isClosed: threadData.isClosed, isHidden: threadData.isHidden, threadPeer: nil)
}
-
+
switch entry.index {
case let .chatList(chatList):
if chatList.pinningIndex != nil {
@@ -758,21 +762,21 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
requiresPremiumForMessaging: entry.isPremiumRequiredToMessage,
displayAsTopicList: entry.displayAsTopicList
))
-
+
if let threadInfo, threadInfo.isHidden {
hiddenGeneralThread = entry
} else {
result.append(entry)
}
}
-
+
if let hiddenGeneralThread {
result.append(hiddenGeneralThread)
}
-
+
if !view.hasLater {
var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1))
-
+
if let savedMessagesPeer = savedMessagesPeer {
if !foundPeers.isEmpty {
var foundPinningIndex: UInt16 = UInt16(foundPeers.count)
@@ -781,7 +785,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
if let chatPeer = peer.1 {
peers[chatPeer.id] = chatPeer
}
-
+
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: peer.0.id, namespace: 0, id: 0), timestamp: 1)
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: foundPinningIndex, messageIndex: messageIndex)),
@@ -818,7 +822,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
}
}
}
-
+
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.predecessor),
presentationData: state.presentationData,
@@ -854,7 +858,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
guard case let .chatList(index) = item.item.index else {
continue
}
-
+
let promoInfo: ChatListNodeEntryPromoInfo
switch item.promoInfo.content {
case .proxy:
@@ -863,10 +867,10 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
promoInfo = .psa(type: type, message: message)
}
let draftState = item.item.draft.flatMap(ChatListItemContent.DraftState.init)
-
+
let peerId = index.messageIndex.id.peerId
let isSelected = state.selectedPeerIds.contains(peerId)
-
+
var threadId: Int64 = 0
switch item.item.index {
case let .forum(_, _, threadIdValue, _, _):
@@ -912,7 +916,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
}
}
}
-
+
if !view.hasLater, case .chatList = mode {
for groupReference in groupItems {
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1)
@@ -938,7 +942,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
hasPinned = true
}
}
-
+
if displayArchiveIntro {
//result.append(.ArchiveIntro(presentationData: state.presentationData))
} else if !contacts.isEmpty && !result.contains(where: { entry in
@@ -950,10 +954,10 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
}) {
result.append(.EmptyIntro(presentationData: state.presentationData))
}
-
+
result.append(.HeaderEntry)
}
-
+
if !view.hasLater {
if case let .peers(_, _, additionalCategories, topPeers, _, _, _) = mode {
var index = 0
@@ -961,7 +965,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
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))
diff --git a/submodules/Display/Source/Font.swift b/submodules/Display/Source/Font.swift
index 4f1a4c0cf8..5fd0f1277e 100644
--- a/submodules/Display/Source/Font.swift
+++ b/submodules/Display/Source/Font.swift
@@ -1,6 +1,11 @@
import Foundation
import UIKit
+// WinterGram: optional custom font overrides. Display is too low-level to read the settings
+// store, so the UI layer pushes the chosen font family names here at startup / on change.
+public var winterGramCustomFontName: String?
+public var winterGramMonoFontName: String?
+
public struct Font {
public enum Design {
case regular
@@ -8,7 +13,7 @@ public struct Font {
case monospace
case round
case camera
-
+
var key: String {
switch self {
case .regular:
@@ -24,28 +29,28 @@ public struct Font {
}
}
}
-
+
public struct Traits: OptionSet {
public var rawValue: Int32
-
+
public init(rawValue: Int32) {
self.rawValue = rawValue
}
-
+
public init() {
self.rawValue = 0
}
-
+
public static let italic = Traits(rawValue: 1 << 0)
public static let monospacedNumbers = Traits(rawValue: 1 << 1)
}
-
+
public enum Width {
case standard
case condensed
case compressed
case expanded
-
+
@available(iOS 16.0, *)
var width: UIFont.Width {
switch self {
@@ -59,7 +64,7 @@ public struct Font {
return .expanded
}
}
-
+
var key: String {
switch self {
case .standard:
@@ -73,7 +78,7 @@ public struct Font {
}
}
}
-
+
public enum Weight {
case regular
case thin
@@ -82,7 +87,7 @@ public struct Font {
case semibold
case bold
case heavy
-
+
var isBold: Bool {
switch self {
case .medium, .semibold, .bold, .heavy:
@@ -91,7 +96,7 @@ public struct Font {
return false
}
}
-
+
var weight: UIFont.Weight {
switch self {
case .thin:
@@ -110,7 +115,7 @@ public struct Font {
return .regular
}
}
-
+
var key: String {
switch self {
case .regular:
@@ -130,17 +135,17 @@ public struct Font {
}
}
}
-
+
private final class Cache {
private var lock: pthread_rwlock_t
private var fonts: [String: UIFont] = [:]
-
+
init() {
self.lock = pthread_rwlock_t()
let status = pthread_rwlock_init(&self.lock, nil)
assert(status == 0)
}
-
+
func get(_ key: String) -> UIFont? {
let font: UIFont?
pthread_rwlock_rdlock(&self.lock)
@@ -148,7 +153,7 @@ public struct Font {
pthread_rwlock_unlock(&self.lock)
return font
}
-
+
func set(_ font: UIFont, key: String) {
pthread_rwlock_wrlock(&self.lock)
self.fonts[key] = font
@@ -157,13 +162,38 @@ public struct Font {
}
private static let cache = Cache()
-
+
public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, width: Width = .standard, traits: Traits = []) -> UIFont {
let key = "\(size)_\(design.key)_\(weight.key)_\(width.key)_\(traits.rawValue)"
-
+
if let cachedFont = self.cache.get(key) {
return cachedFont
}
+
+ // WinterGram: apply a user-chosen font family, if set, for regular and monospace designs.
+ let winterGramOverrideName: String?
+ switch design {
+ case .regular:
+ winterGramOverrideName = winterGramCustomFontName
+ case .monospace:
+ winterGramOverrideName = winterGramMonoFontName
+ default:
+ winterGramOverrideName = nil
+ }
+ if let overrideName = winterGramOverrideName, !overrideName.isEmpty, let baseFont = UIFont(name: overrideName, size: size) {
+ var descriptor = baseFont.fontDescriptor
+ if traits.contains(.italic), let italicDescriptor = descriptor.withSymbolicTraits([descriptor.symbolicTraits, .traitItalic]) {
+ descriptor = italicDescriptor
+ }
+ if weight != .regular {
+ descriptor = descriptor.addingAttributes([
+ UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight.weight]
+ ])
+ }
+ let resultFont = UIFont(descriptor: descriptor, size: size)
+ self.cache.set(resultFont, key: key)
+ return resultFont
+ }
if #available(iOS 13.0, *), design != .camera {
let descriptor: UIFontDescriptor
if #available(iOS 14.0, *) {
@@ -210,16 +240,16 @@ public struct Font {
])
}
}
-
+
let font: UIFont
if let updatedDescriptor = updatedDescriptor {
font = UIFont(descriptor: updatedDescriptor, size: size)
} else {
font = UIFont(descriptor: descriptor, size: size)
}
-
+
self.cache.set(font, key: key)
-
+
return font
} else {
let font: UIFont
@@ -271,25 +301,25 @@ public struct Font {
font = UIFont(name: encodeText(string: "TGDbnfsb.Sfhvmbs", key: -1), size: size) ?? UIFont.systemFont(ofSize: size, weight: weight.weight)
}
}
-
+
self.cache.set(font, key: key)
-
+
return font
}
}
-
+
public static func regular(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size)
}
-
+
public static func medium(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium)
}
-
+
public static func semibold(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold)
}
-
+
public static func bold(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.boldSystemFont(ofSize: size)
@@ -297,15 +327,15 @@ public struct Font {
return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil)
}
}
-
+
public static func heavy(_ size: CGFloat) -> UIFont {
return self.with(size: size, design: .regular, weight: .heavy, traits: [])
}
-
+
public static func light(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light)
}
-
+
public static func semiboldItalic(_ size: CGFloat) -> UIFont {
if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
return UIFont(descriptor: descriptor, size: size)
@@ -313,23 +343,23 @@ public struct Font {
return UIFont.italicSystemFont(ofSize: size)
}
}
-
+
public static func monospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
-
+
public static func semiboldMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
-
+
public static func italicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
-
+
public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
-
+
public static func italic(_ size: CGFloat) -> UIFont {
return UIFont.italicSystemFont(ofSize: size)
}
diff --git a/submodules/Display/Source/NavigationBackgroundView.swift b/submodules/Display/Source/NavigationBackgroundView.swift
index 2fff1fe113..3772792028 100644
--- a/submodules/Display/Source/NavigationBackgroundView.swift
+++ b/submodules/Display/Source/NavigationBackgroundView.swift
@@ -4,26 +4,49 @@ import AsyncDisplayKit
private var sharedIsReduceTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
+// WinterGram Liquid Glass: when enabled, opaque bar backgrounds are made translucent,
+// which activates the blur path below. Display cannot depend on TelegramUIPreferences,
+// so the settings observer in the UI layer pushes values here (main thread).
+public extension Notification.Name {
+ static let winterGramLiquidGlassChanged = Notification.Name("winterGramLiquidGlassChanged")
+}
+
+public struct DisplayLiquidGlass {
+ public static var enabled: Bool = false
+ // Alpha applied to otherwise-opaque backgrounds; (1 - transparency) from settings.
+ public static var alpha: CGFloat = 0.65
+ public static var blurRadius: CGFloat? = nil
+ public static var vibrancy: Bool = true
+}
+
+private func applyLiquidGlass(to color: UIColor) -> UIColor {
+ if DisplayLiquidGlass.enabled, color.alpha >= 0.95 {
+ return color.withAlphaComponent(max(0.05, min(0.9, DisplayLiquidGlass.alpha)))
+ }
+ return color
+}
+
public final class NavigationBackgroundNode: ASDisplayNode {
private var _color: UIColor
+ private var _originalColor: UIColor = .clear
public var color: UIColor {
return self._color
}
-
+
private var enableBlur: Bool
private var enableSaturation: Bool
private var customBlurRadius: CGFloat?
public var effectView: UIVisualEffectView?
private let backgroundNode: ASDisplayNode
-
+
public var backgroundView: UIView {
return self.backgroundNode.view
}
private var validLayout: (CGSize, CGFloat)?
-
+
public var backgroundCornerRadius: CGFloat {
if let (_, cornerRadius) = self.validLayout {
return cornerRadius
@@ -32,6 +55,8 @@ public final class NavigationBackgroundNode: ASDisplayNode {
}
}
+ private var liquidGlassDisposable: Any?
+
public init(color: UIColor, enableBlur: Bool = true, enableSaturation: Bool = true, customBlurRadius: CGFloat? = nil) {
self._color = .clear
self.enableBlur = enableBlur
@@ -45,20 +70,32 @@ public final class NavigationBackgroundNode: ASDisplayNode {
self.addSubnode(self.backgroundNode)
self.updateColor(color: color, transition: .immediate)
+
+ self.liquidGlassDisposable = NotificationCenter.default.addObserver(forName: .winterGramLiquidGlassChanged, object: nil, queue: .main, using: { [weak self] _ in
+ if let strongSelf = self {
+ strongSelf.updateColor(color: strongSelf._originalColor, transition: .animated(duration: 0.2, curve: .easeInOut))
+ }
+ })
}
-
+ deinit {
+ if let liquidGlassDisposable = self.liquidGlassDisposable {
+ NotificationCenter.default.removeObserver(liquidGlassDisposable)
+ }
+ }
+
+
public override func didLoad() {
super.didLoad()
-
+
if self.scheduledUpdate {
self.scheduledUpdate = false
self.updateBackgroundBlur(forceKeepBlur: false)
}
}
-
+
private var scheduledUpdate = false
-
+
private func updateBackgroundBlur(forceKeepBlur: Bool) {
guard self.isNodeLoaded else {
self.scheduledUpdate = true
@@ -113,9 +150,11 @@ public final class NavigationBackgroundNode: ASDisplayNode {
}
public func updateColor(color: UIColor, enableBlur: Bool? = nil, enableSaturation: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) {
+ self._originalColor = color
+ let color = applyLiquidGlass(to: color)
let effectiveEnableBlur = enableBlur ?? self.enableBlur
let effectiveEnableSaturation = enableSaturation ?? self.enableSaturation
-
+
if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur && self.enableSaturation == effectiveEnableSaturation {
return
}
@@ -152,7 +191,7 @@ public final class NavigationBackgroundNode: ASDisplayNode {
effectView.clipsToBounds = !cornerRadius.isZero
}
}
-
+
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) {
self.validLayout = (size, cornerRadius)
@@ -177,6 +216,7 @@ public final class NavigationBackgroundNode: ASDisplayNode {
open class BlurredBackgroundView: UIView {
private var _color: UIColor?
+ private var _originalColor: UIColor?
private var enableBlur: Bool
private var customBlurRadius: CGFloat?
@@ -185,7 +225,7 @@ open class BlurredBackgroundView: UIView {
private let backgroundView: UIView
private var validLayout: (CGSize, CGFloat)?
-
+
public var backgroundCornerRadius: CGFloat {
if let (_, cornerRadius) = self.validLayout {
return cornerRadius
@@ -194,6 +234,8 @@ open class BlurredBackgroundView: UIView {
}
}
+ private var liquidGlassDisposable: Any?
+
public init(color: UIColor?, enableBlur: Bool = true, customBlurRadius: CGFloat? = nil) {
self._color = nil
self.enableBlur = enableBlur
@@ -208,12 +250,24 @@ open class BlurredBackgroundView: UIView {
if let color = color {
self.updateColor(color: color, transition: .immediate)
}
+
+ self.liquidGlassDisposable = NotificationCenter.default.addObserver(forName: .winterGramLiquidGlassChanged, object: nil, queue: .main, using: { [weak self] _ in
+ if let strongSelf = self, let color = strongSelf._originalColor {
+ strongSelf.updateColor(color: color, transition: .animated(duration: 0.2, curve: .easeInOut))
+ }
+ })
}
-
+
+ deinit {
+ if let liquidGlassDisposable = self.liquidGlassDisposable {
+ NotificationCenter.default.removeObserver(liquidGlassDisposable)
+ }
+ }
+
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
private func updateBackgroundBlur(forceKeepBlur: Bool) {
if let color = self._color, self.enableBlur && !sharedIsReduceTransparencyEnabled && ((color.alpha > .ulpOfOne && color.alpha < 0.95) || forceKeepBlur) {
if self.effectView == nil {
@@ -263,6 +317,8 @@ open class BlurredBackgroundView: UIView {
}
public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) {
+ self._originalColor = color
+ let color = applyLiquidGlass(to: color)
let effectiveEnableBlur = enableBlur ?? self.enableBlur
if self._color == color && self.enableBlur == effectiveEnableBlur {
@@ -293,7 +349,7 @@ open class BlurredBackgroundView: UIView {
}
}
}
-
+
if #available(iOS 11.0, *) {
self.backgroundView.layer.maskedCorners = maskedCorners
}
@@ -302,13 +358,13 @@ open class BlurredBackgroundView: UIView {
if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius)
effectView.clipsToBounds = !cornerRadius.isZero
-
+
if #available(iOS 11.0, *) {
effectView.layer.maskedCorners = maskedCorners
}
}
}
-
+
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) {
self.validLayout = (size, cornerRadius)
diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift
index 018b5fc9f8..ffad986e61 100644
--- a/submodules/GalleryUI/Sources/GalleryController.swift
+++ b/submodules/GalleryUI/Sources/GalleryController.swift
@@ -16,6 +16,7 @@ import OpenInExternalAppUI
import ScreenCaptureDetection
import UndoUI
import TranslateUI
+import TelegramUIPreferences
private func tagsForMessage(_ message: Message) -> MessageTags? {
//TODO:rewrite to take all media (effectiveMedia returns all rich-text media; we stop at the first)
@@ -180,7 +181,7 @@ public func internalDocumentItemSupportsMimeType(_ type: String, fileName: Strin
return false
}
}
-
+
if internalMimeTypes.contains(type) {
return true
}
@@ -216,7 +217,7 @@ public func galleryCaptionStringWithAppliedEntities(context: AccountContext, tex
baseQuoteTertiaryTintColor = .clear
}
}
-
+
return stringWithAppliedEntities(
text,
entities: entities,
@@ -288,18 +289,18 @@ public func galleryItemForEntry(
guard let (media, mediaImage) = mediaAndMediaImage else {
return nil
}
-
+
if let image = media as? TelegramMediaImage {
if let file = image.video {
- let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout || message.paidContent != nil || peerIsCopyProtected
-
+ let captureProtected = (message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout || message.paidContent != nil || peerIsCopyProtected) && !currentWinterGramSettings.allowScreenshots
+
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
-
+
let content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
-
+
var (text, entities) = galleryMessageCaptionText(message, mediaSubject: entry.mediaSubject)
if entry.mediaSubject == nil {
if let translateToLanguage, !text.isEmpty {
@@ -348,7 +349,7 @@ public func galleryItemForEntry(
} else if let file = media as? TelegramMediaFile {
if file.isVideo {
let content: UniversalVideoContent
- let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout || message.paidContent != nil || peerIsCopyProtected
+ let captureProtected = (message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout || message.paidContent != nil || peerIsCopyProtected) && !currentWinterGramSettings.allowScreenshots
if file.isAnimated {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else {
@@ -357,7 +358,7 @@ public func galleryItemForEntry(
if #available(iOS 13.0, *) {
if NativeVideoContent.isHLSVideo(file: file) {
isHLS = true
-
+
if let data = context.currentAppConfiguration.with({ $0 }).data, let disableHLS = data["video_ignore_alt_documents"] as? Double {
if Int(disableHLS) != 0 {
isHLS = false
@@ -365,7 +366,7 @@ public func galleryItemForEntry(
}
}
}
-
+
if isHLS {
content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos, autoFetchFullSizeThumbnail: true, codecConfiguration: HLSCodecConfiguration(context: context))
} else {
@@ -375,7 +376,7 @@ public func galleryItemForEntry(
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
}
}
-
+
var (text, entities) = galleryMessageCaptionText(message, mediaSubject: entry.mediaSubject)
if entry.mediaSubject == nil {
if let translateToLanguage, !text.isEmpty {
@@ -388,16 +389,16 @@ public func galleryItemForEntry(
}
}
}
-
+
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
entities = result
}
-
+
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
-
+
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
return UniversalVideoGalleryItem(
context: context,
@@ -502,12 +503,12 @@ public func galleryItemForEntry(
}
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
}
-
+
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
-
+
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
@@ -531,14 +532,14 @@ public func galleryItemForEntry(
)
}
}
-
+
return nil
}
public final class GalleryTransitionArguments {
public let transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))
public let addToTransitionSurface: (UIView) -> Void
-
+
public init(transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: @escaping (UIView) -> Void) {
self.transitionNode = transitionNode
self.addToTransitionSurface = addToTransitionSurface
@@ -548,7 +549,7 @@ public final class GalleryTransitionArguments {
public final class GalleryControllerPresentationArguments {
public let animated: Bool
public let transitionArguments: (MessageId, Media) -> GalleryTransitionArguments?
-
+
public init(animated: Bool = true, transitionArguments: @escaping (MessageId, Media) -> GalleryTransitionArguments?) {
self.animated = animated
self.transitionArguments = transitionArguments
@@ -558,7 +559,7 @@ public final class GalleryControllerPresentationArguments {
private enum GalleryMessageHistoryView {
case view(MessageHistoryView, Bool)
case entries([MessageHistoryEntry], Bool, Bool)
-
+
var entries: [MessageHistoryEntry] {
switch self {
case let .view(view, _):
@@ -567,7 +568,7 @@ private enum GalleryMessageHistoryView {
return entries
}
}
-
+
var tag: HistoryViewInputTag? {
switch self {
case .entries:
@@ -576,7 +577,7 @@ private enum GalleryMessageHistoryView {
return view.tag
}
}
-
+
var hasEarlier: Bool {
switch self {
case let .entries(_, hasEarlier, _):
@@ -585,7 +586,7 @@ private enum GalleryMessageHistoryView {
return view.earlierId != nil
}
}
-
+
var hasLater: Bool {
switch self {
case let .entries(_ , _, hasLater):
@@ -594,7 +595,7 @@ private enum GalleryMessageHistoryView {
return view.laterId != nil
}
}
-
+
var peerIsCopyProtected: Bool {
switch self {
case let .view(_, peerIsCopyProtected):
@@ -623,13 +624,13 @@ public struct GalleryConfiguration {
static var defaultValue: GalleryConfiguration {
return GalleryConfiguration(youtubePictureInPictureEnabled: false)
}
-
+
public let youtubePictureInPictureEnabled: Bool
-
+
fileprivate init(youtubePictureInPictureEnabled: Bool) {
self.youtubePictureInPictureEnabled = youtubePictureInPictureEnabled
}
-
+
static func with(appConfiguration: AppConfiguration) -> GalleryConfiguration {
if let data = appConfiguration.data, let value = data["youtube_pip"] as? String {
return GalleryConfiguration(youtubePictureInPictureEnabled: value != "disabled")
@@ -648,7 +649,7 @@ public struct GalleryEntry {
public var entry: MessageHistoryEntry
public var mediaSubject: GalleryMediaSubject?
public var location: MessageHistoryEntryLocation?
-
+
public var stableId: GalleryEntryStableId {
return GalleryEntryStableId(stableId: self.entry.message.stableId, mediaSubject: self.mediaSubject)
}
@@ -694,38 +695,38 @@ 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, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black, style: .glass)
-
+
private var galleryNode: GalleryControllerNode {
return self.displayNode as! GalleryControllerNode
}
-
+
private let context: AccountContext
private var presentationData: PresentationData
private let source: GalleryControllerItemSource
private let invertItemOrder: Bool
-
+
private let titleView: GalleryTitleView
-
+
private let streamVideos: Bool
-
+
private let _ready = Promise()
override public var ready: Promise {
return self._ready
}
private var didSetReady = false
-
+
private var adjustedForInitialPreviewingLayout = false
-
+
public var temporaryDoNotWaitForReady = false
private let fromPlayingVideo: Bool
private let landscape: Bool
private let timecode: Double?
private var playbackRate: Double?
private var videoQuality: UniversalVideoContentVideoQuality = .auto
-
+
private let accountInUseDisposable = MetaDisposable()
private let disposable = MetaDisposable()
-
+
private var peerIsCopyProtected = false
private var entries: [GalleryEntry] = []
private var hasLeftEntries: Bool = false
@@ -734,7 +735,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
private var tag: HistoryViewInputTag?
private var centralEntryStableId: GalleryEntryStableId?
private var configuration: GalleryConfiguration?
-
+
private let centralItemTitle = Promise()
private let centralItemTitleContent = Promise()
private let centralItemRightBarButtonItem = Promise()
@@ -742,32 +743,32 @@ public class GalleryController: ViewController, StandalonePresentableController,
private let centralItemNavigationStyle = Promise()
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
private let centralItemAttributesDisposable = DisposableSet();
-
+
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
-
+
private let replaceRootController: (ViewController, Promise?) -> Void
private let baseNavigationController: NavigationController?
-
+
private var hiddenMediaManagerIndex: Int?
-
+
private let actionInteraction: GalleryControllerActionInteraction?
private var performAction: (GalleryControllerInteractionTapAction) -> Void
private var openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
-
+
private let updateVisibleDisposable = MetaDisposable()
-
+
private var screenCaptureEventsDisposable: Disposable?
-
+
private let generateStoreAfterDownload: ((Message, TelegramMediaFile) -> (() -> Void)?)?
-
+
public var centralItemUpdated: ((MessageId) -> Void)?
public var onDidAppear: (() -> Void)?
public var useSimpleAnimation: Bool = false
-
+
public var navigateToMessageContext: ((EngineMessage) -> Void)?
-
+
private var initialOrientation: UIInterfaceOrientation?
-
+
public init(
context: AccountContext,
source: GalleryControllerItemSource,
@@ -802,28 +803,28 @@ public class GalleryController: ViewController, StandalonePresentableController,
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: file), peerId: message.id.peerId).start()
}
}
-
+
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
-
+
var performActionImpl: ((GalleryControllerInteractionTapAction) -> Void)?
self.performAction = { action in
performActionImpl?(action)
}
-
+
var openActionOptionsImpl: ((GalleryControllerInteractionTapAction, Message) -> Void)?
self.openActionOptions = { action, message in
openActionOptionsImpl?(action, message)
}
-
+
self.titleView = GalleryTitleView(context: context, presentationData: self.presentationData)
-
+
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
-
+
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = backItem
-
+
self.statusBar.statusBarStyle = .White
-
+
let baseLanguageCode = self.presentationData.strings.baseLanguageCode
let message: Signal<(Message, Bool)?, NoError>
var translateToLanguage: Signal = .single(nil)
@@ -843,7 +844,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
if peerIdValue == context.account.peerId, let customTag {
context.engine.messages.internalReindexSavedMessagesCustomTagsIfNeeded(threadId: threadIdValue, tag: customTag)
}
-
+
message = context.account.postbox.transaction { transaction -> (Message, Bool)? in
guard let message = transaction.getMessage(messageId) else {
return nil
@@ -918,14 +919,14 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
|> take(1)
-
+
let semaphore: DispatchSemaphore?
if synchronousLoad {
semaphore = DispatchSemaphore(value: 0)
} else {
semaphore = nil
}
-
+
var displayInfoOnTop = false
if case .custom = source {
displayInfoOnTop = true
@@ -942,18 +943,18 @@ public class GalleryController: ViewController, StandalonePresentableController,
if let strongSelf = self {
if let view = view {
strongSelf.peerIsCopyProtected = view.peerIsCopyProtected
-
+
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration)
strongSelf.configuration = configuration
-
+
var mediaSubject: GalleryMediaSubject?
if case let .standaloneMessage(_, mediaSubjectValue) = source {
mediaSubject = mediaSubjectValue
}
-
+
let entries = galleryEntriesForMessageHistoryEntries(view.entries, mediaSubject: mediaSubject)
-
+
var centralEntryStableId: GalleryEntryStableId?
loop: for i in 0 ..< entries.count {
let entry = entries[i]
@@ -976,9 +977,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
strongSelf.tag = view.tag
-
+
if invertItemOrder {
strongSelf.entries = entries.reversed()
strongSelf.hasLeftEntries = view.hasLater
@@ -1027,7 +1028,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
) {
if isCentral {
centralItemIndex = items.count
-
+
if isFirstTime {
isFirstTime = false
if item is UniversalVideoGalleryItem {
@@ -1039,9 +1040,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(item)
}
}
-
+
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
-
+
if strongSelf.temporaryDoNotWaitForReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
@@ -1070,27 +1071,27 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}))
-
+
if let semaphore = semaphore {
let _ = semaphore.wait(timeout: DispatchTime.now() + 1.0)
}
-
+
var syncResultApply: (() -> Void)?
let _ = syncResult.modify { processed, f in
syncResultApply = f
return (true, nil)
}
-
+
syncResultApply?()
-
+
self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in
self?.navigationItem.title = title
}))
-
+
self.centralItemAttributesDisposable.add(self.centralItemTitleContent.get().start(next: { [weak self] titleContent in
self?.titleView.setContent(content: titleContent)
}))
-
+
self.centralItemAttributesDisposable.add(combineLatest(self.centralItemRightBarButtonItem.get(), self.centralItemRightBarButtonItems.get()).start(next: { [weak self] rightBarButtonItem, rightBarButtonItems in
if let rightBarButtonItem = rightBarButtonItem {
self?.navigationItem.rightBarButtonItem = rightBarButtonItem
@@ -1101,13 +1102,13 @@ public class GalleryController: ViewController, StandalonePresentableController,
self?.navigationItem.rightBarButtonItems = nil
}
}))
-
+
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, overlayContentNode in
self?.galleryNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode).withUpdatedOverlayContentNode(overlayContentNode)
}, transition: .animated(duration: 0.4, curve: .spring))
}))
-
+
self.centralItemAttributesDisposable.add(self.centralItemNavigationStyle.get().start(next: { [weak self] style in
if let strongSelf = self {
switch style {
@@ -1124,7 +1125,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}))
-
+
let mediaManager = context.sharedContext.mediaManager
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get()
|> map { messageIdAndMedia in
@@ -1134,7 +1135,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
return nil
}
})
-
+
performActionImpl = { [weak self] action in
if let strongSelf = self {
if case let .url(_, _, _, dismiss) = action, !dismiss {
@@ -1165,7 +1166,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
openActionOptionsImpl = { [weak self] action, message in
if let strongSelf = self {
var presentationData = strongSelf.presentationData
@@ -1181,7 +1182,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
let telString = "tel:"
var openText = presentationData.strings.Conversation_LinkDialogOpen
var phoneNumber: String?
-
+
var isEmail = false
var isPhoneNumber = false
if cleanUrl.hasPrefix(mailtoString) {
@@ -1198,7 +1199,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
openText = presentationData.strings.Conversation_FileOpenIn
}
let actionSheet = ActionSheetController(presentationData: presentationData)
-
+
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: cleanUrl))
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
@@ -1224,7 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(ActionSheetButtonItem(title: canAddToReadingList ? presentationData.strings.ShareMenu_CopyShareLink : presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = cleanUrl
-
+
let content: UndoOverlayContent
if isPhoneNumber {
content = .copy(text: presentationData.strings.Conversation_PhoneCopied)
@@ -1261,7 +1262,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
-
+
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self, let peer = peer {
@@ -1274,7 +1275,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}))
@@ -1299,7 +1300,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
@@ -1316,7 +1317,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = command
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}))
@@ -1340,7 +1341,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_HashtagCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
@@ -1358,7 +1359,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
} else {
isCopyLink = false
}
-
+
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
@@ -1379,14 +1380,14 @@ public class GalleryController: ViewController, StandalonePresentableController,
|> deliverOnMainQueue).start(next: { link in
if let link = link {
UIPasteboard.general.string = link + "?t=\(Int32(timecode))"
-
+
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
-
+
var warnAboutPrivate = false
if channel.addressName == nil {
warnAboutPrivate = true
}
-
+
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent
if warnAboutPrivate {
@@ -1398,14 +1399,14 @@ public class GalleryController: ViewController, StandalonePresentableController,
})
} else {
UIPasteboard.general.string = text
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
})
} else {
UIPasteboard.general.string = text
-
+
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
@@ -1422,14 +1423,14 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.isOpaqueWhenInOverlay = true
-
+
switch source {
case let .peerMessagesAtId(id, _, _, _):
- if id.peerId.namespace == Namespaces.Peer.SecretChat {
+ if id.peerId.namespace == Namespaces.Peer.SecretChat && !currentWinterGramSettings.allowScreenshots {
self.screenCaptureEventsDisposable = (screenCaptureEvents()
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self, strongSelf.traceVisibility() {
@@ -1441,16 +1442,16 @@ public class GalleryController: ViewController, StandalonePresentableController,
break
}
}
-
+
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
deinit {
if let initialOrientation = self.initialOrientation {
self.context.sharedContext.applicationBindings.forceOrientation(initialOrientation)
}
-
+
self.accountInUseDisposable.dispose()
self.disposable.dispose()
self.centralItemAttributesDisposable.dispose()
@@ -1460,25 +1461,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.updateVisibleDisposable.dispose()
self.screenCaptureEventsDisposable?.dispose()
}
-
+
@objc private func donePressed() {
self.dismiss(forceAway: false)
}
-
+
func willDismiss() {
if let chatController = self.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .immediate)
}
}
-
+
func dismiss(forceAway: Bool) {
var animatedOutNode = true
var animatedOutInterface = false
-
+
if forceAway {
self._hiddenMedia.set(.single(nil))
}
-
+
let completion = { [weak self] in
if animatedOutNode && animatedOutInterface {
self?.actionInteraction?.updateCanReadHistory(true)
@@ -1486,11 +1487,11 @@ public class GalleryController: ViewController, StandalonePresentableController,
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
-
+
if let chatController = self.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
}
-
+
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
let entry = self.entries[centralItemNode.index]
let message = entry.entry.message
@@ -1502,13 +1503,13 @@ public class GalleryController: ViewController, StandalonePresentableController,
})
}
}
-
+
self.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
}
-
+
override public func loadDisplayNode() {
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
if let strongSelf = self {
@@ -1534,7 +1535,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
snapshotView.frame = strongSelf.galleryNode.footerNode.frame
snapshots.append(snapshotView)
}
-
+
strongSelf.actionInteraction?.editMedia(messageId, snapshots, { [weak self] in
self?.dismiss(forceAway: true)
})
@@ -1544,14 +1545,14 @@ public class GalleryController: ViewController, StandalonePresentableController,
}, currentItemNode: { [weak self] in
return self?.galleryNode.pager.centralItemNode()
})
-
+
let disableTapNavigation = !(self.context.sharedContext.currentMediaDisplaySettings.with { $0 }.showNextMediaOnTap)
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, titleView: titleView, disableTapNavigation: disableTapNavigation)
self.displayNodeDidLoad()
-
+
self.galleryNode.statusBar = self.statusBar
self.galleryNode.navigationBar = self.navigationBar
-
+
self.galleryNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments {
@@ -1569,25 +1570,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
-
+
self.galleryNode.beginCustomDismiss = { [weak self] animationType in
if let strongSelf = self {
strongSelf.actionInteraction?.updateCanReadHistory(true)
strongSelf._hiddenMedia.set(.single(nil))
-
+
if let hiddenMediaManagerIndex = strongSelf.hiddenMediaManagerIndex {
strongSelf.hiddenMediaManagerIndex = nil
strongSelf.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
-
+
switch animationType {
case .default, .simpleAnimation:
let animatedOutNode = animationType != .simpleAnimation
-
+
if let chatController = strongSelf.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
}
-
+
strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
})
case .pip:
@@ -1595,17 +1596,17 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in
guard let self else {
return
}
-
+
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex {
self.hiddenMediaManagerIndex = nil
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
-
+
if isPictureInPicture {
if let chatController = self.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
@@ -1613,21 +1614,21 @@ public class GalleryController: ViewController, StandalonePresentableController,
} else {
self._hiddenMedia.set(.single(nil))
}
-
+
self.presentingViewController?.dismiss(animated: false, completion: nil)
}
-
+
self.galleryNode.controlsVisibilityChanged = { [weak self] visible in
guard let self else {
return
}
self.prefersOnScreenNavigationHidden = !visible
-
+
self.galleryNode.pager.forEachItemNode { itemNode in
itemNode.controlsVisibilityUpdated(isVisible: visible, animated: true)
}
}
-
+
self.galleryNode.updateOrientation = { [weak self] orientation in
if let strongSelf = self {
if strongSelf.initialOrientation == nil {
@@ -1638,7 +1639,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
strongSelf.context.sharedContext.applicationBindings.forceOrientation(orientation)
}
}
-
+
let baseNavigationController = self.baseNavigationController
self.galleryNode.baseNavigationController = { [weak baseNavigationController] in
return baseNavigationController
@@ -1646,12 +1647,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.galleryNode.galleryController = { [weak self] in
return self
}
-
+
var displayInfoOnTop = false
if case .custom = source {
displayInfoOnTop = true
}
-
+
var items: [GalleryItem] = []
var centralItemIndex: Int?
for entry in self.entries {
@@ -1688,21 +1689,21 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(item)
}
}
-
+
self.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
-
+
self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var hiddenItem: (MessageId, Media)?
if let index = index {
let entry = strongSelf.entries[index]
let message = strongSelf.entries[index].entry.message
-
+
strongSelf.centralEntryStableId = entry.stableId
if let selectedMedia = selectedMediaForMessage(message: message, mediaSubject: entry.mediaSubject) {
hiddenItem = (message.id, selectedMedia)
}
-
+
if let node = strongSelf.galleryNode.pager.centralItemNode() {
strongSelf.centralItemTitle.set(node.title())
strongSelf.centralItemTitleContent.set(node.titleContent())
@@ -1712,7 +1713,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
strongSelf.centralItemFooterContentNode.set(node.footerContent())
strongSelf.galleryNode.pager.pagingEnabledPromise.set(node.isPagingEnabled())
}
-
+
switch strongSelf.source {
case let .peerMessagesAtId(_, chatLocation, _, chatLocationContextHolder):
var reloadAroundIndex: MessageIndex?
@@ -1737,15 +1738,15 @@ public class GalleryController: ViewController, StandalonePresentableController,
return .single(mapped)
}
|> take(1)
-
+
strongSelf.updateVisibleDisposable.set((signal
|> deliverOnMainQueue).start(next: { view in
guard let strongSelf = self, let view = view else {
return
}
-
+
let entries = galleryEntriesForMessageHistoryEntries(view.entries, mediaSubject: nil)
-
+
if strongSelf.invertItemOrder {
strongSelf.entries = entries.reversed()
strongSelf.hasLeftEntries = view.hasLater
@@ -1774,7 +1775,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(item)
}
}
-
+
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
}
}))
@@ -1783,13 +1784,13 @@ public class GalleryController: ViewController, StandalonePresentableController,
if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries && !strongSelf.loadingMore {
strongSelf.loadingMore = true
loadMore?()
-
+
strongSelf.updateVisibleDisposable.set((messages
|> deliverOnMainQueue).start(next: { messages, totalCount, hasMore in
guard let strongSelf = self else {
return
}
-
+
var messageEntries: [MessageHistoryEntry] = []
var index = messages.count
for message in messages.reversed() {
@@ -1797,7 +1798,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
index -= 1
}
let entries = galleryEntriesForMessageHistoryEntries(messageEntries, mediaSubject: nil)
-
+
if entries.count > strongSelf.entries.count {
if strongSelf.invertItemOrder {
strongSelf.entries = entries.reversed()
@@ -1827,10 +1828,10 @@ public class GalleryController: ViewController, StandalonePresentableController,
items.append(item)
}
}
-
+
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
}
-
+
strongSelf.updateVisibleDisposable.set(nil)
strongSelf.loadingMore = false
}
@@ -1848,7 +1849,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
if !self.entries.isEmpty && !self.didSetReady {
if self.temporaryDoNotWaitForReady {
self.didSetReady = true
@@ -1861,19 +1862,19 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
-
+
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
-
+
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex {
self.hiddenMediaManagerIndex = nil
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
-
+
let context = self.context
let mediaManager = context.sharedContext.mediaManager
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get()
@@ -1884,12 +1885,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
return nil
}
})
-
+
var nodeAnimatesItself = false
-
+
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
let entry = self.entries[centralItemNode.index]
-
+
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleContent.set(centralItemNode.titleContent())
self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem())
@@ -1905,7 +1906,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
}
-
+
self._hiddenMedia.set(.single((message.id, selectedMedia)))
}
centralItemNode.activateAsInitial()
@@ -1913,7 +1914,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.onDidAppear?()
}
-
+
if !self.isPresentedInPreviewingContext() {
//self.galleryNode.setControlsHidden(self.landscape, animated: false)
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
@@ -1922,12 +1923,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
self.accountInUseDisposable.set(self.context.sharedContext.setAccountUserInterfaceInUse(self.context.account.id))
-
+
self.actionInteraction?.updateCanReadHistory(false)
}
-
+
override public func didAppearInContextPreview() {
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
let message = self.entries[centralItemNode.index].entry.message
@@ -1938,19 +1939,19 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled())
-
+
if !mediaForMessage(message: message).isEmpty {
centralItemNode.activateAsInitial()
}
}
}
-
+
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
-
+
self.accountInUseDisposable.set(nil)
}
-
+
override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() {
return itemSize.aspectFitted(layout.size)
@@ -1958,16 +1959,16 @@ public class GalleryController: ViewController, StandalonePresentableController,
return nil
}
}
-
+
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
-
+
self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
-
+
if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() {
self.navigationBar?.isHidden = true
-
+
self.adjustedForInitialPreviewingLayout = true
self.galleryNode.setControlsHidden(true, animated: false)
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() {
@@ -1987,7 +1988,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
func updateSharedVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
self.videoQuality = videoQuality
@@ -1997,7 +1998,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
}
-
+
public var keyShortcuts: [KeyShortcut] {
var keyShortcuts: [KeyShortcut] = []
keyShortcuts.append(
@@ -2060,7 +2061,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
keyShortcuts.append(contentsOf: itemNodeShortcuts)
return keyShortcuts
}
-
+
public static func maybeExpandPIP(context: AccountContext, messageId: EngineMessage.Id) -> Bool {
guard let currentPictureInPictureNode = context.sharedContext.mediaManager.currentPictureInPictureNode as? UniversalVideoGalleryItemNode else {
return false
@@ -2074,12 +2075,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
if message.id != messageId {
return false
}
-
+
currentPictureInPictureNode.expandPIP()
-
+
return true
}
-
+
func dismissAndNavigateToMessageContext(message: Message) {
if let navigateToMessageContext = self.navigateToMessageContext {
navigateToMessageContext(EngineMessage(message))
diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift
index 3390fbcace..afb27fdffd 100644
--- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift
+++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift
@@ -13,6 +13,7 @@ import AppBundle
import LocalizedPeerData
import TooltipUI
import TelegramNotices
+import TelegramUIPreferences
private func galleryMediaForMedia(media: Media) -> Media? {
if let media = media as? TelegramMediaImage {
@@ -57,9 +58,9 @@ private func mediaForMessage(message: Message) -> Media? {
private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
fileprivate var timeoutNode: RadialStatusNode?
-
+
private var validLayout: (ContainerViewLayout, CGFloat)?
-
+
var beginTimeAndTimeout: (Double, Double, Bool)? {
didSet {
if let (beginTime, timeout, isOutgoing) = self.beginTimeAndTimeout {
@@ -69,13 +70,13 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
self.timeoutNode = timeoutNode
let icon: RadialStatusNodeState.SecretTimeoutIcon
let timeoutValue = Int32(timeout)
-
+
let state: RadialStatusNodeState
if timeoutValue == 0 && isOutgoing {
state = .staticTimeout
} else if timeoutValue == viewOnceTimeout {
beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
-
+
if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) {
icon = .image(image)
} else {
@@ -87,9 +88,9 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
}
timeoutNode.transitionToState(state, completion: {})
self.addSubnode(timeoutNode)
-
+
timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside)
-
+
if let (layout, navigationHeight) = self.validLayout {
self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
@@ -100,42 +101,42 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
}
}
}
-
+
var statusPressed: (UIView) -> Void = { _ in }
@objc private func statusTapGesture() {
if let sourceView = self.timeoutNode?.view {
self.statusPressed(sourceView)
}
}
-
+
var onDismissTransitionUpdate: (CGFloat) -> Void = { _ in }
-
+
override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) {
super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation)
-
+
self.timeoutNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
-
+
override func animateOut(animateContent: Bool, completion: @escaping () -> Void) {
super.animateOut(animateContent: animateContent, completion: completion)
-
+
if let timeoutNode = self.timeoutNode {
timeoutNode.layer.animateAlpha(from: timeoutNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
-
+
override func updateDismissTransition(_ value: CGFloat) {
self.timeoutNode?.alpha = value
self.onDismissTransitionUpdate(value)
}
-
+
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
-
+
self.validLayout = (layout, navigationBarHeight)
self.layoutTimeoutNode(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
-
+
private func layoutTimeoutNode(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if let timeoutNode = self.timeoutNode {
let diameter: CGFloat = 28.0
@@ -147,51 +148,51 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
public final class SecretMediaPreviewController: ViewController {
private let context: AccountContext
private let messageId: MessageId
-
+
private let _ready = Promise()
override public var ready: Promise {
return self._ready
}
private var didSetReady = false
-
+
private let disposable = MetaDisposable()
private let markMessageAsConsumedDisposable = MetaDisposable()
-
+
private var controllerNode: SecretMediaPreviewControllerNode {
return self.displayNode as! SecretMediaPreviewControllerNode
}
-
+
private var messageView: MessageView?
private var currentNodeMessageId: MessageId?
private var currentNodeMessageIsVideo = false
private var currentNodeMessageIsViewOnce = false
private var currentMessageIsDismissed = false
private var tempFile: TempBoxFile?
-
+
private let centralItemAttributesDisposable = DisposableSet();
private let footerContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
-
+
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
private var hiddenMediaManagerIndex: Int?
-
+
private let presentationData: PresentationData
-
+
private var screenCaptureEventsDisposable: Disposable?
-
+
private weak var tooltipController: TooltipScreen?
-
+
public init(context: AccountContext, messageId: MessageId) {
self.context = context
self.messageId = messageId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
-
+
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
-
+
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = backItem
-
+
self.statusBar.statusBarStyle = .White
-
+
self.disposable.set((context.account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in
if let strongSelf = self {
strongSelf.messageView = view
@@ -200,7 +201,7 @@ public final class SecretMediaPreviewController: ViewController {
}
}
}))
-
+
self.hiddenMediaManagerIndex = self.context.sharedContext.mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get()
|> map { messageIdAndMedia in
if let (messageId, media) = messageIdAndMedia {
@@ -209,7 +210,7 @@ public final class SecretMediaPreviewController: ViewController {
return nil
}
})
-
+
self.centralItemAttributesDisposable.add(self.footerContentNode.get().start(next: { [weak self] footerContentNode, _ in
guard let self else {
return
@@ -219,11 +220,11 @@ public final class SecretMediaPreviewController: ViewController {
}, transition: .immediate)
}))
}
-
+
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
deinit {
self.disposable.dispose()
self.markMessageAsConsumedDisposable.dispose()
@@ -236,11 +237,11 @@ public final class SecretMediaPreviewController: ViewController {
}
self.centralItemAttributesDisposable.dispose()
}
-
+
@objc func donePressed() {
self.dismiss(forceAway: false)
}
-
+
public override func loadDisplayNode() {
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
if let strongSelf = self {
@@ -258,22 +259,22 @@ public final class SecretMediaPreviewController: ViewController {
})
self.displayNode = SecretMediaPreviewControllerNode(context: self.context, controllerInteraction: controllerInteraction, titleView: nil)
self.displayNodeDidLoad()
-
+
self.controllerNode.statusPressed = { [weak self] _ in
if let self {
self.presentViewOnceTooltip()
}
}
-
+
self.controllerNode.onDismissTransitionUpdate = { [weak self] _ in
if let self {
self.dismissAllTooltips()
}
}
-
+
self.controllerNode.statusBar = self.statusBar
self.controllerNode.navigationBar = self.navigationBar
-
+
self.controllerNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.controllerNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments {
@@ -290,23 +291,23 @@ public final class SecretMediaPreviewController: ViewController {
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
-
+
self.controllerNode.beginCustomDismiss = { [weak self] _ in
if let strongSelf = self {
strongSelf._hiddenMedia.set(.single(nil))
-
+
let animatedOutNode = true
-
+
strongSelf.controllerNode.animateOut(animateContent: animatedOutNode, completion: {
})
}
}
-
+
self.controllerNode.completeCustomDismiss = { [weak self] _ in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
-
+
self.controllerNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var hiddenItem: (MessageId, Media)?
@@ -319,12 +320,12 @@ public final class SecretMediaPreviewController: ViewController {
videoDuration = file.duration
}
}
-
+
var timerStarted = false
let isOutgoing = !message.flags.contains(.Incoming)
if let attribute = message.autoclearAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
-
+
if let countdownBeginTime = attribute.countdownBeginTime {
timerStarted = true
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
@@ -337,7 +338,7 @@ public final class SecretMediaPreviewController: ViewController {
}
} else if let attribute = message.autoremoveAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
-
+
if let countdownBeginTime = attribute.countdownBeginTime {
timerStarted = true
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
@@ -349,7 +350,7 @@ public final class SecretMediaPreviewController: ViewController {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
}
-
+
if let file = media as? TelegramMediaFile {
if file.isAnimated {
strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title
@@ -367,11 +368,11 @@ public final class SecretMediaPreviewController: ViewController {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title
}
}
-
+
if let beginTimeAndTimeout = beginTimeAndTimeout {
strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
-
+
if message.flags.contains(.Incoming) || strongSelf.currentNodeMessageIsVideo {
if let node = strongSelf.controllerNode.pager.centralItemNode() {
strongSelf.footerContentNode.set(node.footerContent())
@@ -408,16 +409,16 @@ public final class SecretMediaPreviewController: ViewController {
}
}
}
-
+
if let _ = self.messageView {
self.applyMessageView()
}
}
-
+
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
-
- if self.screenCaptureEventsDisposable == nil {
+
+ if self.screenCaptureEventsDisposable == nil && !currentWinterGramSettings.allowScreenshots {
self.screenCaptureEventsDisposable = (screenCaptureEvents()
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self, strongSelf.traceVisibility() {
@@ -429,33 +430,33 @@ public final class SecretMediaPreviewController: ViewController {
}
})
}
-
+
var nodeAnimatesItself = false
-
+
if let centralItemNode = self.controllerNode.pager.centralItemNode(), let message = self.messageView?.message {
if let media = mediaForMessage(message: message) {
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
nodeAnimatesItself = true
centralItemNode.activateAsInitial()
-
+
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
}
-
+
self._hiddenMedia.set(.single((message.id, media)))
} else if self.isPresentedInPreviewingContext() {
centralItemNode.activateAsInitial()
}
}
}
-
+
self.controllerNode.setControlsHidden(false, animated: false)
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
if presentationArguments.animated {
self.controllerNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false)
}
}
-
+
if self.currentNodeMessageIsViewOnce {
let _ = (ApplicationSpecificNotice.incrementViewOnceTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
@@ -468,20 +469,20 @@ public final class SecretMediaPreviewController: ViewController {
})
}
}
-
+
private func dismiss(forceAway: Bool) {
self.dismissAllTooltips()
-
+
var animatedOutNode = true
var animatedOutInterface = false
-
+
let completion = { [weak self] in
if animatedOutNode && animatedOutInterface {
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
-
+
if let centralItemNode = self.controllerNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let message = self.messageView?.message {
if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway {
animatedOutNode = false
@@ -491,13 +492,13 @@ public final class SecretMediaPreviewController: ViewController {
})
}
}
-
+
self.controllerNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
}
-
+
private func applyMessageView() {
var message: Message?
if let messageView = self.messageView, let m = messageView.message {
@@ -526,7 +527,7 @@ public final class SecretMediaPreviewController: ViewController {
break
}
}
-
+
let entry = GalleryEntry(entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
if let self {
@@ -542,7 +543,7 @@ public final class SecretMediaPreviewController: ViewController {
self._ready.set(.single(true))
return
}
-
+
self.controllerNode.pager.replaceItems([item], centralItemIndex: 0)
let ready = self.controllerNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
self?.didSetReady = true
@@ -557,7 +558,7 @@ public final class SecretMediaPreviewController: ViewController {
videoDuration = file.duration
}
}
-
+
let isOutgoing = !message.flags.contains(.Incoming)
if let attribute = message.autoclearAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
@@ -580,7 +581,7 @@ public final class SecretMediaPreviewController: ViewController {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
}
-
+
if self.isNodeLoaded {
if let beginTimeAndTimeout = beginTimeAndTimeout {
self.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
@@ -597,24 +598,24 @@ public final class SecretMediaPreviewController: ViewController {
self.currentMessageIsDismissed = true
}
}
-
+
private func dismissAllTooltips() {
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
-
+
private func presentViewOnceTooltip() {
guard self.currentNodeMessageIsViewOnce, let sourceView = self.controllerNode.timeoutNode?.view else {
return
}
-
+
self.dismissAllTooltips()
-
+
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize())
-
+
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let iconName = "anim_autoremove_on"
let text: String
@@ -623,7 +624,7 @@ public final class SecretMediaPreviewController: ViewController {
} else {
text = presentationData.strings.Gallery_ViewOncePhotoTooltip
}
-
+
let tooltipController = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
@@ -644,14 +645,14 @@ public final class SecretMediaPreviewController: ViewController {
self.tooltipController = tooltipController
self.present(tooltipController, in: .window(.root))
}
-
+
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
-
+
self.controllerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
-
+
override public func dismiss(completion: (() -> Void)? = nil) {
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift
index 23ea3eb290..89fb3dd3a7 100644
--- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift
+++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift
@@ -18,18 +18,18 @@ private let productIdentifiers = [
"org.telegram.telegramPremium.threeMonths.code_x1",
"org.telegram.telegramPremium.sixMonths.code_x1",
"org.telegram.telegramPremium.twelveMonths.code_x1",
-
+
"org.telegram.telegramPremium.threeMonths.code_x5",
"org.telegram.telegramPremium.sixMonths.code_x5",
"org.telegram.telegramPremium.twelveMonths.code_x5",
-
+
"org.telegram.telegramPremium.threeMonths.code_x10",
"org.telegram.telegramPremium.sixMonths.code_x10",
"org.telegram.telegramPremium.twelveMonths.code_x10",
-
+
"org.telegram.telegramPremium.oneWeek.auth",
"org.telegram.telegramPremium.threeDays.auth",
-
+
"org.telegram.telegramStars.topup.x15",
"org.telegram.telegramStars.topup.x25",
"org.telegram.telegramStars.topup.x50",
@@ -59,7 +59,7 @@ private extension NSDecimalNumber {
raiseOnUnderflow: false,
raiseOnDivideByZero: false))
}
-
+
func prettyPrice() -> NSDecimalNumber {
return self.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
@@ -85,17 +85,17 @@ public final class InAppPurchaseManager: NSObject {
numberFormatter.locale = self.skProduct.priceLocale
return numberFormatter
}()
-
+
let skProduct: SKProduct
-
+
init(skProduct: SKProduct) {
self.skProduct = skProduct
}
-
+
public var id: String {
return self.skProduct.productIdentifier
}
-
+
public var isSubscription: Bool {
if #available(iOS 12.0, *) {
return self.skProduct.subscriptionGroupIdentifier != nil
@@ -103,16 +103,16 @@ public final class InAppPurchaseManager: NSObject {
return self.skProduct.subscriptionPeriod != nil
}
}
-
+
public var price: String {
return self.numberFormatter.string(from: self.skProduct.price) ?? ""
}
-
+
public func pricePerMonth(_ monthsCount: Int) -> String {
let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2)
return self.numberFormatter.string(from: price) ?? ""
}
-
+
public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String {
let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2)
let prettierPrice = price
@@ -131,7 +131,7 @@ public final class InAppPurchaseManager: NSObject {
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
-
+
public func multipliedPrice(count: Int) -> String {
let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2)
let prettierPrice = price
@@ -150,11 +150,11 @@ public final class InAppPurchaseManager: NSObject {
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
-
+
public var priceValue: NSDecimalNumber {
return self.skProduct.price
}
-
+
public var priceCurrencyAndAmount: (currency: String, amount: Int64) {
if let currencyCode = self.numberFormatter.currencyCode,
let amount = fractionalToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) {
@@ -163,7 +163,7 @@ public final class InAppPurchaseManager: NSObject {
return ("", 0)
}
}
-
+
public static func ==(lhs: Product, rhs: Product) -> Bool {
if lhs.id != rhs.id {
return false
@@ -176,13 +176,13 @@ public final class InAppPurchaseManager: NSObject {
}
return true
}
-
+
}
-
+
public enum PurchaseState {
case purchased(transactionId: String)
}
-
+
public enum PurchaseError {
case generic
case cancelled
@@ -192,23 +192,23 @@ public final class InAppPurchaseManager: NSObject {
case assignFailed
case tryLater
}
-
+
public enum RestoreState {
case succeed(Bool)
case failed
}
-
+
private final class PaymentTransactionContext {
var state: SKPaymentTransactionState?
let purpose: PendingInAppPurchaseState.Purpose
let subscriber: (TransactionState) -> Void
-
+
init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) {
self.purpose = purpose
self.subscriber = subscriber
}
}
-
+
private enum TransactionState {
case purchased(transactionId: String?)
case restored(transactionId: String?)
@@ -217,51 +217,51 @@ public final class InAppPurchaseManager: NSObject {
case assignFailed
case deferred
}
-
+
private let engine: SomeTelegramEngine
-
+
private var products: [Product] = []
private var productsPromise = Promise<[Product]>([])
private var productRequest: SKProductsRequest?
-
+
private let stateQueue = Queue()
private var paymentContexts: [String: PaymentTransactionContext] = [:]
-
+
private var finishedSuccessfulTransactions = Set()
-
+
private var onRestoreCompletion: ((RestoreState) -> Void)?
-
+
private let disposableSet = DisposableDict()
-
+
private var lastRequestTimestamp: Double?
public init(engine: SomeTelegramEngine) {
self.engine = engine
-
+
super.init()
-
+
SKPaymentQueue.default().add(self)
self.requestProducts()
}
-
+
deinit {
SKPaymentQueue.default().remove(self)
}
-
+
var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
-
+
private func requestProducts() {
Logger.shared.log("InAppPurchaseManager", "Requesting products")
let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
productRequest.delegate = self
productRequest.start()
-
+
self.productRequest = productRequest
self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent()
}
-
+
public var availableProducts: Signal<[Product], NoError> {
if self.products.isEmpty {
if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 {
@@ -271,30 +271,36 @@ public final class InAppPurchaseManager: NSObject {
}
return self.productsPromise.get()
}
-
+
public func restorePurchases(completion: @escaping (RestoreState) -> Void) {
Logger.shared.log("InAppPurchaseManager", "Restoring purchases")
self.onRestoreCompletion = completion
-
+
let paymentQueue = SKPaymentQueue.default()
paymentQueue.restoreCompletedTransactions()
}
-
+
public func finishAllTransactions() {
Logger.shared.log("InAppPurchaseManager", "Finishing all transactions")
-
+
let paymentQueue = SKPaymentQueue.default()
let transactions = paymentQueue.transactions
for transaction in transactions {
paymentQueue.finishTransaction(transaction)
}
}
-
+
public func buyProduct(_ product: Product, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal {
+ // WinterGram does not support in-app purchases; subscriptions bought in the
+ // official Telegram app apply to this account automatically.
+ return .fail(.cantMakePayments)
+ }
+
+ private func unusedOriginalBuyProduct(_ product: Product, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal {
if !self.canMakePayments {
return .fail(.cantMakePayments)
}
-
+
let accountPeerId: String
switch self.engine {
case let .authorized(engine):
@@ -302,20 +308,20 @@ public final class InAppPurchaseManager: NSObject {
case let .unauthorized(engine):
accountPeerId = "\(engine.account.id.int64)"
}
-
+
Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)")
-
+
let purpose = PendingInAppPurchaseState.Purpose(appStorePurpose: purpose)
-
+
let payment = SKMutablePayment(product: product.skProduct)
payment.applicationUsername = accountPeerId
payment.quantity = Int(quantity)
SKPaymentQueue.default().add(payment)
-
+
let productIdentifier = payment.productIdentifier
let signal = Signal { subscriber in
let disposable = MetaDisposable()
-
+
self.stateQueue.async {
let paymentContext = PaymentTransactionContext(purpose: purpose, subscriber: { state in
switch state {
@@ -356,7 +362,7 @@ public final class InAppPurchaseManager: NSObject {
}
})
self.paymentContexts[productIdentifier] = paymentContext
-
+
disposable.set(ActionDisposable { [weak paymentContext] in
self.stateQueue.async {
if let current = self.paymentContexts[productIdentifier], current === paymentContext {
@@ -365,18 +371,18 @@ public final class InAppPurchaseManager: NSObject {
}
})
}
-
+
return disposable
}
return signal
}
-
+
public struct ReceiptPurchase: Equatable {
public let productId: String
public let transactionId: String
public let expirationDate: Date
}
-
+
public func getReceiptPurchases() -> [ReceiptPurchase] {
guard let data = getReceiptData(), let receipt = parseReceipt(data) else {
return []
@@ -388,10 +394,10 @@ public final class InAppPurchaseManager: NSObject {
extension InAppPurchaseManager: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.productRequest = nil
-
+
Queue.mainQueue().async {
let products = response.products.map { Product(skProduct: $0) }
-
+
Logger.shared.log("InAppPurchaseManager", "Received products \(products.map({ $0.skProduct.productIdentifier }).joined(separator: ", "))")
self.productsPromise.set(.single(products))
}
@@ -420,15 +426,15 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
case let .unauthorized(engine):
accountPeerId = "\(engine.account.id.int64)"
}
-
+
let paymentContexts = self.paymentContexts
-
+
var transactionsToAssign: [SKPaymentTransaction] = []
for transaction in transactions {
if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId {
continue
}
-
+
let productIdentifier = transaction.payment.productIdentifier
let transactionState: TransactionState?
switch transaction.transactionState {
@@ -475,24 +481,24 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
}
-
+
if !transactionsToAssign.isEmpty {
let transactionIds = transactionsToAssign.compactMap({ $0.transactionIdentifier }).joined(separator: ", ")
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), sending receipt for transactions [\(transactionIds)]")
-
+
guard let transaction = transactionsToAssign.first else {
return
}
let productIdentifier = transaction.payment.productIdentifier
-
+
var completion: Signal = .never()
-
+
let products = self.availableProducts
|> filter { products in
return !products.isEmpty
}
|> take(1)
-
+
let product: Signal = products
|> map { products in
if let product = products.first(where: { $0.id == productIdentifier }) {
@@ -501,7 +507,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
return nil
}
}
-
+
let purpose: Signal
if let paymentContext = paymentContexts[productIdentifier] {
purpose = product
@@ -521,20 +527,20 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
}
-
+
completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil)
-
+
let receiptData = getReceiptData() ?? Data()
#if DEBUG
self.debugSaveReceipt(receiptData: receiptData)
#endif
-
+
for transaction in transactionsToAssign {
if let transactionIdentifier = transaction.transactionIdentifier {
self.finishedSuccessfulTransactions.insert(transactionIdentifier)
}
}
-
+
self.disposableSet.set(
(purpose
|> castError(AssignAppStoreTransactionError.self)
@@ -560,7 +566,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
for transaction in transactions {
queue.finishTransaction(transaction)
}
-
+
let _ = completion.start()
}),
forKey: transactionIds
@@ -568,13 +574,13 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
}
-
+
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
Queue.mainQueue().async {
if let onRestoreCompletion = self.onRestoreCompletion {
Logger.shared.log("InAppPurchaseManager", "Transactions restoration finished")
self.onRestoreCompletion = nil
-
+
if let receiptData = getReceiptData() {
let signal: Signal
switch self.engine {
@@ -606,7 +612,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
}
-
+
public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
Queue.mainQueue().async {
if let onRestoreCompletion = self.onRestoreCompletion {
@@ -616,7 +622,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
}
-
+
private func debugSaveReceipt(receiptData: Data) {
guard case let .authorized(engine) = self.engine else {
return
@@ -638,12 +644,12 @@ private final class PendingInAppPurchaseState: Codable {
case purpose
case storeProductId
}
-
+
enum Purpose: Codable {
enum DecodingError: Error {
case generic
}
-
+
enum CodingKeys: String, CodingKey {
case type
case peer
@@ -665,7 +671,7 @@ private final class PendingInAppPurchaseState: Codable {
case phoneCodeHash
case premiumDays
}
-
+
enum PurposeType: Int32 {
case subscription
case upgrade
@@ -678,7 +684,7 @@ private final class PendingInAppPurchaseState: Codable {
case starsGiveaway
case authCode
}
-
+
case subscription
case upgrade
case restore
@@ -689,7 +695,7 @@ private final class PendingInAppPurchaseState: Codable {
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, premiumDays: Int32)
-
+
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -757,10 +763,10 @@ private final class PendingInAppPurchaseState: Codable {
throw DecodingError.generic
}
}
-
+
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
-
+
switch self {
case .subscription:
try container.encode(PurposeType.subscription.rawValue, forKey: .type)
@@ -815,7 +821,7 @@ private final class PendingInAppPurchaseState: Codable {
try container.encode(premiumDays, forKey: .premiumDays)
}
}
-
+
init(appStorePurpose: AppStoreTransactionPurpose) {
switch appStorePurpose {
case .subscription:
@@ -840,7 +846,7 @@ private final class PendingInAppPurchaseState: Codable {
self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, premiumDays: premiumDays)
}
}
-
+
func appStorePurpose(product: InAppPurchaseManager.Product?) -> AppStoreTransactionPurpose {
let (currency, amount) = product?.priceCurrencyAndAmount ?? ("", 0)
switch self {
@@ -867,22 +873,22 @@ private final class PendingInAppPurchaseState: Codable {
}
}
}
-
+
public let productId: String
public let purpose: Purpose
-
+
public init(productId: String, purpose: Purpose) {
self.productId = productId
self.purpose = purpose
}
-
+
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.productId = try container.decode(String.self, forKey: .productId)
self.purpose = try container.decode(Purpose.self, forKey: .purpose)
}
-
+
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
@@ -894,7 +900,7 @@ private final class PendingInAppPurchaseState: Codable {
private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String) -> Signal {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue))
-
+
switch engine {
case let .authorized(engine):
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key))
@@ -912,8 +918,8 @@ private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: St
private func updatePendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue))
-
-
+
+
switch engine {
case let .authorized(engine):
if let content = content {
diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift
index e1f339963a..349df1af21 100644
--- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift
+++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift
@@ -25,70 +25,70 @@ private final class ShimmerEffectNode: ASDisplayNode {
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
-
+
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
-
+
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
-
+
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
-
+
super.init()
-
+
self.isLayerBacked = true
self.clipsToBounds = true
-
+
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
-
+
override func didEnterHierarchy() {
super.didEnterHierarchy()
-
+
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
-
+
override func didExitHierarchy() {
super.didExitHierarchy()
-
+
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
-
+
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
-
+
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
-
+
context.clip(to: CGRect(origin: CGPoint(), size: size))
-
+
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
-
+
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
-
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
-
+
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
-
+
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
@@ -96,19 +96,19 @@ private final class ShimmerEffectNode: ASDisplayNode {
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
-
+
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
-
+
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
}
-
+
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
@@ -120,7 +120,7 @@ private final class ShimmerEffectNode: ASDisplayNode {
}
}
}
-
+
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
@@ -139,57 +139,57 @@ private final class LoadingShimmerNode: ASDisplayNode {
case circle(CGRect)
case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat)
}
-
+
private let backgroundNode: ASDisplayNode
private let effectNode: ShimmerEffectNode
private let foregroundNode: ASImageNode
-
+
private var currentShapes: [Shape] = []
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private var currentShimmeringColor: UIColor?
private var currentSize = CGSize()
-
+
override init() {
self.backgroundNode = ASDisplayNode()
-
+
self.effectNode = ShimmerEffectNode()
-
+
self.foregroundNode = ASImageNode()
self.foregroundNode.displaysAsynchronously = false
self.foregroundNode.displayWithoutProcessing = true
-
+
super.init()
-
+
self.addSubnode(self.backgroundNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.foregroundNode)
}
-
+
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.effectNode.updateAbsoluteRect(rect, within: containerSize)
}
-
+
func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) {
if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size {
return
}
-
+
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.currentShimmeringColor = shimmeringColor
self.currentShapes = shapes
self.currentSize = size
-
+
self.backgroundNode.backgroundColor = foregroundColor
-
+
self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor)
-
+
self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(), size: size))
-
+
context.setFillColor(UIColor.clear.cgColor)
for shape in shapes {
switch shape {
@@ -202,7 +202,7 @@ private final class LoadingShimmerNode: ASDisplayNode {
}
}
})
-
+
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
@@ -214,7 +214,7 @@ public struct ItemListPeerItemEditing: Equatable {
public var editing: Bool
public var canBeReordered: Bool
public var revealed: Bool?
-
+
public init(editable: Bool, editing: Bool, canBeReordered: Bool = false, revealed: Bool?) {
self.editable = editable
self.editing = editing
@@ -234,7 +234,7 @@ public enum ItemListPeerItemText {
case accent
case constructive
}
-
+
case presence
case text(String, TextColor)
case none
@@ -257,7 +257,7 @@ public struct ItemListPeerItemSwitch {
public var value: Bool
public var style: ItemListPeerItemSwitchStyle
public var isEnabled: Bool
-
+
public init(value: Bool, style: ItemListPeerItemSwitchStyle, isEnabled: Bool = true) {
self.value = value
self.style = style
@@ -297,7 +297,7 @@ public struct ItemListPeerItemRevealOption {
public var type: ItemListPeerItemRevealOptionType
public var title: String
public var action: () -> Void
-
+
public init(type: ItemListPeerItemRevealOptionType, title: String, action: @escaping () -> Void) {
self.type = type
self.title = title
@@ -307,7 +307,7 @@ public struct ItemListPeerItemRevealOption {
public struct ItemListPeerItemRevealOptions {
public var options: [ItemListPeerItemRevealOption]
-
+
public init(options: [ItemListPeerItemRevealOption]) {
self.options = options
}
@@ -315,7 +315,7 @@ public struct ItemListPeerItemRevealOptions {
public struct ItemListPeerItemShimmering {
public var alternationIndex: Int
-
+
public init(alternationIndex: Int) {
self.alternationIndex = alternationIndex
}
@@ -347,10 +347,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
self.resolveInlineStickers = resolveInlineStickers
}
}
-
+
case account(AccountContext)
case custom(Custom)
-
+
public var accountPeerId: EnginePeer.Id {
switch self {
case let .account(context):
@@ -359,7 +359,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.accountPeerId
}
}
-
+
public var engine: TelegramEngine {
switch self {
case let .account(context):
@@ -368,7 +368,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.engine
}
}
-
+
public var animationCache: AnimationCache {
switch self {
case let .account(context):
@@ -377,7 +377,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.animationCache
}
}
-
+
public var animationRenderer: MultiAnimationRenderer {
switch self {
case let .account(context):
@@ -386,7 +386,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.animationRenderer
}
}
-
+
public var isPremiumDisabled: Bool {
switch self {
case let .account(context):
@@ -395,7 +395,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.isPremiumDisabled
}
}
-
+
public var resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> {
switch self {
case let .account(context):
@@ -406,7 +406,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return custom.resolveInlineStickers
}
}
-
+
public var energyUsageSettings: EnergyUsageSettings {
switch self {
case let .account(context):
@@ -415,7 +415,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
return .default
}
}
-
+
public var contentSettings: ContentSettings {
switch self {
case let .account(context):
@@ -425,7 +425,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
}
}
}
-
+
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let dateTimeFormat: PresentationDateTimeFormat
@@ -472,7 +472,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
public var hasActiveRevealOptions: Bool {
return self.editing.revealed == true
}
-
+
public init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle = .legacy,
@@ -560,7 +560,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
self.storyStats = storyStats
self.openStories = openStories
}
-
+
public init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle = .legacy,
@@ -648,15 +648,15 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
self.storyStats = storyStats
self.openStories = openStories
}
-
+
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListPeerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem))
-
+
node.contentSize = layout.contentSize
node.insets = layout.insets
-
+
Queue.mainQueue().async {
completion(node, {
return (node.avatarNode.ready, { _ in apply(synchronousLoads, false) })
@@ -664,7 +664,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
}
}
}
-
+
private func getHeaderAtTop(top: ListViewItem?, bottom: ListViewItem?) -> Bool {
var headerAtTop = false
if let top = top as? ItemListPeerItem, top.header != nil {
@@ -674,20 +674,20 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
} else if self.header != nil {
headerAtTop = true
}
-
+
return headerAtTop
}
-
+
public 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) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListPeerItemNode {
let makeLayout = nodeValue.asyncLayout()
-
+
var animated = true
if case .None = animation {
animated = false
}
-
+
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem))
Queue.mainQueue().async {
@@ -699,7 +699,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealO
}
}
}
-
+
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
@@ -715,20 +715,20 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private let maskNode: ASImageNode
-
+
private let containerNode: ContextControllerSourceNode
public override var controlsContainer: ASDisplayNode {
return self.containerNode
}
-
+
fileprivate let avatarNode: AvatarNode
private var avatarIconComponent: EmojiStatusComponent?
private var avatarIconView: ComponentView?
-
+
private var customAvatarIconView: UIImageView?
-
+
private var avatarButton: HighlightTrackingButton?
-
+
private let titleNode: TextNode
private let labelNode: TextNodeWithEntities
private let labelBadgeNode: ASImageNode
@@ -738,19 +738,21 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var credibilityIconView: ComponentHostView?
private var verifiedIconComponent: EmojiStatusComponent?
private var verifiedIconView: ComponentHostView?
+ private var winterGramIconComponent: EmojiStatusComponent?
+ private var winterGramIconView: ComponentHostView?
private var switchNode: SwitchNode?
private var checkNode: ASImageNode?
private var leftCheckNode: CheckNode?
-
+
private var shimmerNode: LoadingShimmerNode?
private var absoluteLocation: (CGRect, CGSize)?
-
+
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)?
-
+
private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode?
-
+
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
@@ -766,7 +768,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
}
-
+
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
@@ -797,7 +799,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
}
-
+
override public var canBeSelected: Bool {
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
return false
@@ -808,66 +810,66 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
return false
}
}
-
+
public var tag: ItemListItemTag? {
return self.layoutParams?.0.tag
}
-
+
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
-
+
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
-
+
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
-
+
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
-
+
self.containerNode = ContextControllerSourceNode()
-
+
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)))
//self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
-
+
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
-
+
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
-
+
self.labelNode = TextNodeWithEntities()
-
+
self.labelBadgeNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true
self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true
-
+
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
-
+
super.init(layerBacked: false, rotated: false, seeThrough: false)
-
+
self.isAccessibilityElement = true
-
+
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.labelNode.textNode)
-
+
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3)
apply(false, true)
}
})
-
+
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let contextAction = item.contextAction else {
gesture.cancel()
@@ -876,13 +878,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
contextAction(strongSelf.containerNode, gesture)
}
}
-
+
override public func didLoad() {
super.didLoad()
-
+
self.updateEnableGestures()
}
-
+
private func updateEnableGestures() {
if let item = self.layoutParams?.0, item.disableInteractiveTransitionIfNecessary, let revealOptions = item.revealOptions, !revealOptions.options.isEmpty {
self.view.disablesInteractiveTransitionGestureRecognizer = true
@@ -890,42 +892,42 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.view.disablesInteractiveTransitionGestureRecognizer = false
}
}
-
+
public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
-
+
var currentDisabledOverlayNode = self.disabledOverlayNode
-
+
var currentSwitchNode = self.switchNode
var currentCheckNode = self.checkNode
-
+
let currentLabelArrowNode = self.labelArrowNode
-
+
let currentItem = self.layoutParams?.0
-
+
let currentHasBadge = self.labelBadgeNode.image != nil
-
+
return { item, params, neighbors, headerAtTop in
var updateArrowImage: UIImage?
-
+
let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)
-
+
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let statusFont = Font.regular(statusFontSize)
let labelFont = Font.regular(labelFontSize)
let labelDisclosureFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
-
+
var updatedLabelBadgeImage: UIImage?
var credibilityIcon: EmojiStatusComponent.Content?
var credibilityParticleColor: UIColor?
var verifiedIcon: EmojiStatusComponent.Content?
-
+
if case .threatSelfAsSaved = item.aliasHandling, item.peer.id == item.context.accountPeerId {
} else {
if item.peer.isScam {
@@ -937,10 +939,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
if let color = emojiStatus.color {
credibilityParticleColor = UIColor(rgb: UInt32(bitPattern: color))
}
- } else if item.peer.isPremium && !item.context.isPremiumDisabled {
+ } else if item.peer.isPremium && !currentWinterGramSettings.hidePremiumStatuses && !item.context.isPremiumDisabled {
credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor)
}
-
+
if item.peer.isVerified {
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
credibilityParticleColor = nil
@@ -949,7 +951,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
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))
}
}
-
+
var titleIconsWidth: CGFloat = 0.0
if let verifiedIcon = verifiedIcon {
titleIconsWidth += 4.0
@@ -973,14 +975,18 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
titleIconsWidth += 16.0
}
}
-
+ let isWinterGramOfficial = isWinterGramOfficialPeer(item.peer)
+ if isWinterGramOfficial {
+ titleIconsWidth += 4.0 + 16.0
+ }
+
var badgeColor: UIColor?
if case .badge = item.label {
badgeColor = item.presentationData.theme.list.itemAccentColor
} else if case let .text(_, _, color, hasBackground) = item.label, let color, hasBackground {
badgeColor = color.withMultipliedAlpha(0.1)
}
-
+
let badgeDiameter: CGFloat = 20.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
@@ -990,11 +996,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else if let badgeColor = badgeColor, !currentHasBadge {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
-
+
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
var labelAttributedString: NSAttributedString?
-
+
let peerRevealOptions: [ItemListRevealOption]
if item.editing.editable && item.enabled {
if let revealOptions = item.revealOptions {
@@ -1027,13 +1033,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else {
peerRevealOptions = []
}
-
+
var additionalLeftInset: CGFloat = 0.0
var leftInset: CGFloat = params.leftInset
var rightInset: CGFloat = params.rightInset
var switchSize = CGSize(width: 51.0, height: 31.0)
var checkImage: UIImage?
-
+
if let switchValue = item.switchValue {
switch switchValue.style {
case .standard:
@@ -1059,14 +1065,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
currentSwitchNode = nil
currentCheckNode = nil
}
-
+
if let currentSwitchNode, let switchView = currentSwitchNode.view as? UISwitch {
if currentSwitchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
switchSize = switchView.bounds.size
}
-
+
let titleColor: UIColor
switch item.nameColor {
case .primary:
@@ -1074,7 +1080,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
case .secret:
titleColor = item.presentationData.theme.chatList.secretTitleColor
}
-
+
let currentBoldFont: UIFont
switch item.nameStyle {
case .distinctBold:
@@ -1082,7 +1088,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
case .plain:
currentBoldFont = titleFont
}
-
+
if let threadInfo = item.threadInfo {
titleAttributedString = NSAttributedString(string: threadInfo.title, font: currentBoldFont, textColor: titleColor)
} else if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling {
@@ -1117,7 +1123,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else if case let .channel(channel) = item.peer {
titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor)
}
-
+
switch item.text {
case .presence:
if case let .user(user) = item.peer, let botInfo = user.botInfo {
@@ -1188,17 +1194,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
leftInset += 65.0
avatarFontSize = floor(40.0 * 16.0 / 37.0)
}
-
+
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
-
+
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
-
+
if item.editing.canBeReordered {
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply
@@ -1207,7 +1213,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else {
editingOffset = 0.0
}
-
+
var labelMaximumNumberOfLines = 1
var labelInset: CGFloat = 0.0
var labelAlignment: NSTextAlignment = .natural
@@ -1249,17 +1255,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
labelInset += 15.0
}
-
+
labelInset += reorderInset
-
+
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: labelMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: labelAlignment, lineSpacing: labelLineSpacing, cutout: nil, insets: UIEdgeInsets()))
-
+
let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
let constrainedStatusSize = CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude)
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedStatusSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
var insets = itemListNeighborsGroupedInsets(neighbors, params)
if !item.hasTopGroupInset {
switch neighbors.top {
@@ -1276,19 +1282,19 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
if headerAtTop, let header = item.header {
insets.top += header.height + 18.0
}
-
+
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
-
+
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height
-
+
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
-
+
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
-
+
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
@@ -1297,16 +1303,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else {
currentDisabledOverlayNode = nil
}
-
+
return (layout, { [weak self] synchronousLoad, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors, headerAtTop)
-
+
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
-
+
strongSelf.avatarNode.font = avatarPlaceholderFont(size: avatarFontSize)
-
+
strongSelf.accessibilityLabel = titleAttributedString?.string
var combinedValueString = ""
if let statusString = statusAttributedString?.string, !statusString.isEmpty {
@@ -1315,13 +1321,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
if let labelString = labelAttributedString?.string, !labelString.isEmpty {
combinedValueString.append(", \(labelString)")
}
-
+
strongSelf.accessibilityValue = combinedValueString
-
+
if let updateArrowImage = updateArrowImage {
strongSelf.labelArrowNode?.image = updateArrowImage
}
-
+
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
@@ -1332,23 +1338,23 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
}
-
+
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
-
+
strongSelf.backgroundNode.isHidden = !item.displayBackground
-
+
let revealOffset = strongSelf.revealOffset
-
+
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
-
+
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
@@ -1365,7 +1371,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
})
strongSelf.disabledOverlayNode = nil
}
-
+
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil {
@@ -1395,7 +1401,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
editableControlNode?.removeFromSupernode()
})
}
-
+
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
@@ -1412,7 +1418,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
reorderControlNode?.removeFromSupernode()
})
}
-
+
let _ = titleApply()
let _ = statusApply()
if case let .account(context) = item.context {
@@ -1421,7 +1427,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let _ = labelApply(nil)
}
strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil
-
+
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
@@ -1434,7 +1440,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
-
+
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
@@ -1461,22 +1467,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
bottomStripeIsHidden = hasCorners || !item.displayDecorations
}
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
-
+
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.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)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
-
+
var titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)
-
+
var titleLeftOffset: CGFloat = 0.0
var nextIconX: CGFloat = titleFrame.maxX
if let verifiedIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
-
+
var verifiedIconTransition = transition
let verifiedIconView: ComponentHostView
if let current = strongSelf.verifiedIconView {
@@ -1487,7 +1493,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.containerNode.view.addSubview(verifiedIconView)
strongSelf.verifiedIconView = verifiedIconView
}
-
+
let verifiedIconComponent = EmojiStatusComponent(
postbox: item.context.engine.account.postbox,
energyUsageSettings: item.context.energyUsageSettings,
@@ -1500,16 +1506,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
emojiFileUpdated: nil
)
strongSelf.verifiedIconComponent = verifiedIconComponent
-
+
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
environment: {},
containerSize: CGSize(width: 16.0, height: 16.0)
)
-
+
verifiedIconTransition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
-
+
titleLeftOffset += iconSize.width + 4.0
nextIconX += iconSize.width + 4.0
} else if let verifiedIconView = strongSelf.verifiedIconView {
@@ -1517,14 +1523,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
verifiedIconView.removeFromSuperview()
}
titleFrame = titleFrame.offsetBy(dx: titleLeftOffset, dy: 0.0)
-
+
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
-
+
if let credibilityIcon = credibilityIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
-
+
var creditibilityIconTransition = transition
let credibilityIconView: ComponentHostView
if let current = strongSelf.credibilityIconView {
@@ -1535,7 +1541,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.containerNode.view.addSubview(credibilityIconView)
strongSelf.credibilityIconView = credibilityIconView
}
-
+
let credibilityIconComponent = EmojiStatusComponent(
postbox: item.context.engine.account.postbox,
energyUsageSettings: item.context.energyUsageSettings,
@@ -1555,14 +1561,59 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
-
+
nextIconX += 4.0
creditibilityIconTransition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
+ // Advance past the credibility icon so a following badge (e.g. the WinterGram
+ // snowflake) lands to its right instead of overlapping it.
+ nextIconX += iconSize.width
} else if let credibilityIconView = strongSelf.credibilityIconView {
strongSelf.credibilityIconView = nil
credibilityIconView.removeFromSuperview()
}
-
+
+ if isWinterGramOfficial {
+ let animationCache = item.context.animationCache
+ let animationRenderer = item.context.animationRenderer
+
+ var winterGramIconTransition = transition
+ let winterGramIconView: ComponentHostView
+ if let current = strongSelf.winterGramIconView {
+ winterGramIconView = current
+ } else {
+ winterGramIconTransition = .immediate
+ winterGramIconView = ComponentHostView()
+ strongSelf.containerNode.view.addSubview(winterGramIconView)
+ strongSelf.winterGramIconView = winterGramIconView
+ }
+
+ let winterGramIconComponent = EmojiStatusComponent(
+ postbox: item.context.engine.account.postbox,
+ energyUsageSettings: item.context.energyUsageSettings,
+ resolveInlineStickers: item.context.resolveInlineStickers,
+ animationCache: animationCache,
+ animationRenderer: animationRenderer,
+ content: isWinterGramOfficialPeer(item.peer) ? .winterGramBadge(backplateColor: winterGramBadgeBackplateColor(theme: item.presentationData.theme)) : EmojiStatusComponent.Content.none,
+ isVisibleForAnimations: strongSelf.visibilityStatus,
+ action: nil,
+ emojiFileUpdated: nil
+ )
+ strongSelf.winterGramIconComponent = winterGramIconComponent
+ let iconSize = winterGramIconView.update(
+ transition: .immediate,
+ component: AnyComponent(winterGramIconComponent),
+ environment: {},
+ containerSize: CGSize(width: 16.0, height: 16.0)
+ )
+
+ nextIconX += 4.0
+ winterGramIconTransition.updateFrame(view: winterGramIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
+ nextIconX += iconSize.width
+ } else if let winterGramIconView = strongSelf.winterGramIconView {
+ strongSelf.winterGramIconView = nil
+ winterGramIconView.removeFromSuperview()
+ }
+
if let currentSwitchNode = currentSwitchNode {
if currentSwitchNode !== strongSelf.switchNode {
strongSelf.switchNode = currentSwitchNode
@@ -1581,7 +1632,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
switchNode.removeFromSupernode()
strongSelf.switchNode = nil
}
-
+
if let currentCheckNode = currentCheckNode {
if currentCheckNode !== strongSelf.checkNode {
strongSelf.checkNode = currentCheckNode
@@ -1598,9 +1649,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
checkNode.removeFromSupernode()
strongSelf.checkNode = nil
}
-
+
var rightLabelInset: CGFloat = 15.0 + params.rightInset
-
+
if let updatedLabelArrowNode = updatedLabelArrowNode {
strongSelf.labelArrowNode = updatedLabelArrowNode
strongSelf.containerNode.addSubnode(updatedLabelArrowNode)
@@ -1613,7 +1664,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
labelArrowNode.removeFromSupernode()
strongSelf.labelArrowNode = nil
}
-
+
if let updateBadgeImage = updatedLabelBadgeImage {
if strongSelf.labelBadgeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode.textNode)
@@ -1624,19 +1675,19 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.labelBadgeNode.image = nil
strongSelf.labelBadgeNode.removeFromSupernode()
}
-
+
let badgeWidth = max(badgeDiameter, labelLayout.size.width + 12.0)
var labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
if strongSelf.labelBadgeNode.image != nil {
labelFrame.origin.x -= 6.0
}
transition.updateFrame(node: strongSelf.labelNode.textNode, frame: labelFrame)
-
+
strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(labelFrame.midX - badgeWidth * 0.5), y: floorToScreenPixels(labelFrame.midY - badgeDiameter * 0.5)), size: CGSize(width: badgeWidth, height: badgeDiameter))
-
+
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + additionalLeftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
-
+
if item.storyStats != nil {
let avatarButton: HighlightTrackingButton
if let current = strongSelf.avatarButton {
@@ -1652,7 +1703,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.avatarButton = nil
avatarButton.removeFromSuperview()
}
-
+
if let switchValue = item.switchValue, case .leftCheck = switchValue.style {
let leftCheckNode: CheckNode
if let current = strongSelf.leftCheckNode {
@@ -1673,13 +1724,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
leftCheckNode.removeFromSupernode()
}
}
-
+
if let threadInfo = item.threadInfo {
let threadIconSize = floor(avatarSize * 0.9)
let threadIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - threadIconSize) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - threadIconSize) / 2.0)), size: CGSize(width: threadIconSize, height: threadIconSize))
-
+
strongSelf.avatarNode.isHidden = true
-
+
let avatarIconView: ComponentView
if let current = strongSelf.avatarIconView {
avatarIconView = current
@@ -1687,14 +1738,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
avatarIconView = ComponentView()
strongSelf.avatarIconView = avatarIconView
}
-
+
let avatarIconContent: EmojiStatusComponent.Content
if let fileId = threadInfo.icon, fileId != 0 {
avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .forever)
} else {
avatarIconContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: threadIconFrame.size)
}
-
+
let avatarIconComponent = EmojiStatusComponent(
postbox: item.context.engine.account.postbox,
energyUsageSettings: item.context.energyUsageSettings,
@@ -1713,7 +1764,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
environment: {},
containerSize: threadIconFrame.size
)
-
+
if let avatarIconComponentView = avatarIconView.view {
if avatarIconComponentView.superview == nil {
strongSelf.containerNode.view.addSubview(avatarIconComponentView)
@@ -1751,12 +1802,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
overrideImage = .deletedIcon
}
strongSelf.avatarNode.imageNode.animateFirstTransition = item.animateFirstAvatarTransition
-
+
var clipStyle: AvatarNodeClipStyle = .round
if case let .channel(channel) = item.peer, channel.isForumOrMonoForum {
clipStyle = .roundedRect
}
-
+
strongSelf.avatarNode.setPeer(
accountPeerId: item.context.accountPeerId,
postbox: item.context.engine.account.postbox,
@@ -1769,7 +1820,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
clipStyle: clipStyle,
synchronousLoad: synchronousLoad
)
-
+
strongSelf.avatarNode.setStoryStats(storyStats: item.storyStats.flatMap { storyStats in
return AvatarNode.StoryStats(
totalCount: storyStats.totalCount,
@@ -1784,17 +1835,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
), transition: .immediate)
}
}
-
+
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
-
+
if let presence = item.presence {
strongSelf.peerPresenceManager?.reset(presence: presence)
}
-
+
if let shimmering = item.shimmering {
strongSelf.avatarNode.isHidden = true
strongSelf.titleNode.isHidden = true
-
+
let shimmerNode: LoadingShimmerNode
if let current = strongSelf.shimmerNode {
shimmerNode = current
@@ -1824,14 +1875,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else if let shimmerNode = strongSelf.shimmerNode {
strongSelf.avatarNode.isHidden = false
strongSelf.titleNode.isHidden = false
-
+
strongSelf.shimmerNode = nil
shimmerNode.removeFromSupernode()
}
-
+
if let customAvatarIcon = item.customAvatarIcon {
strongSelf.avatarNode.isHidden = true
-
+
let customAvatarIconView: UIImageView
if let current = strongSelf.customAvatarIconView {
customAvatarIconView = current
@@ -1841,31 +1892,31 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.containerNode.view.addSubview(customAvatarIconView)
}
customAvatarIconView.image = customAvatarIcon
-
+
transition.updateFrame(view: customAvatarIconView, frame: strongSelf.avatarNode.frame)
} else if let customAvatarIconView = strongSelf.customAvatarIconView {
strongSelf.customAvatarIconView = nil
customAvatarIconView.removeFromSuperview()
}
-
+
strongSelf.backgroundNode.isHidden = !item.displayDecorations
strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations || !item.highlightable
-
+
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
-
+
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
if let revealed = item.editing.revealed {
strongSelf.setRevealOptionsOpened(revealed, animated: animated)
}
-
+
strongSelf.updateIsHighlighted(transition: transition)
}
})
}
}
-
+
var isHighlighted = false
-
+
var reallyHighlighted: Bool {
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
if let (item, _, _, _) = self.layoutParams, item.highlighted {
@@ -1873,38 +1924,38 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
return reallyHighlighted
}
-
+
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.reallyHighlighted, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
-
+
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
-
+
if let avatarButton = self.avatarButton, avatarButton.bounds.contains(self.view.convert(point, to: avatarButton)) {
self.isHighlighted = false
} else {
self.isHighlighted = highlighted
-
+
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
}
-
+
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
-
+
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
-
+
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
-
+
guard let item = self.layoutParams?.0, let params = self.layoutParams?.1 else {
return
}
-
+
let leftInset: CGFloat
switch item.height {
case .generic:
@@ -1912,7 +1963,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
case .peerList:
leftInset = 65.0 + params.leftInset
}
-
+
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
@@ -1922,22 +1973,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else {
editingOffset = 0.0
}
-
+
var titleLeftOffset: CGFloat = 0.0
if let verifiedIconView = self.verifiedIconView {
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verifiedIconView.frame.minY), size: verifiedIconView.bounds.size))
titleLeftOffset += verifiedIconView.bounds.size.width + 4.0
}
-
+
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset + titleLeftOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size))
-
+
if let credibilityIconView = self.credibilityIconView {
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 4.0, y: credibilityIconView.frame.minY), size: credibilityIconView.bounds.size))
}
-
+
var rightLabelInset: CGFloat = 15.0 + params.rightInset
-
+
if let labelArrowNode = self.labelArrowNode {
if let image = labelArrowNode.image {
let labelArrowNodeFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - image.size.width + 8.0, y: labelArrowNode.frame.minY), size: image.size)
@@ -1945,35 +1996,35 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
rightLabelInset += 19.0
}
}
-
+
let badgeDiameter: CGFloat = 20.0
let labelSize = self.labelNode.textNode.frame.size
-
+
let badgeWidth = max(badgeDiameter, labelSize.width + 12.0)
var labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.textNode.bounds.size.width - rightLabelInset, y: self.labelNode.textNode.frame.minY), size: self.labelNode.textNode.bounds.size)
if self.labelBadgeNode.image != nil {
labelFrame.origin.x -= 6.0
}
-
+
transition.updateFrame(node: self.labelNode.textNode, frame: labelFrame)
-
+
transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(labelFrame.midX - badgeWidth * 0.5), y: floorToScreenPixels(labelFrame.midY - badgeDiameter * 0.5)), size: CGSize(width: badgeWidth, height: badgeDiameter)))
-
+
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let avatarButton = self.avatarButton {
avatarButton.frame = avatarFrame
}
-
+
if let customAvatarIconView = self.customAvatarIconView {
transition.updateFrame(view: customAvatarIconView, frame: avatarFrame)
}
-
+
if let avatarIconComponentView = self.avatarIconView?.view {
let avatarFrame = self.avatarNode.frame
let threadIconSize = floor(avatarFrame.width * 0.9)
let threadIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - threadIconSize) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - threadIconSize) / 2.0)), size: CGSize(width: threadIconSize, height: threadIconSize))
-
+
transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame)
}
}
@@ -1983,23 +2034,23 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.updateIsHighlighted(transition: transition)
}
-
+
override public func revealOptionsInteractivelyOpened() {
if let (item, _, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(item.peer.id, nil)
}
}
-
+
override public func revealOptionsInteractivelyClosed() {
if let (item, _, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(nil, item.peer.id)
}
}
-
+
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
-
+
if let (item, _, _, _) = self.layoutParams {
if let revealOptions = item.revealOptions {
if option.key >= 0 && option.key < Int32(revealOptions.options.count) {
@@ -2010,13 +2061,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
}
-
+
private func toggleUpdated(_ value: Bool) {
if let (item, _, _, _) = self.layoutParams {
item.toggleUpdated?(value)
}
}
-
+
override public func headers() -> [ListViewItemHeader]? {
if let item = self.layoutParams?.0 {
return item.header.flatMap { [$0] }
@@ -2024,7 +2075,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
return nil
}
}
-
+
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
@@ -2033,14 +2084,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
-
+
override public func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
return true
}
return false
}
-
+
@objc private func avatarButtonPressed() {
guard let item = self.layoutParams?.0 else {
return
@@ -2061,9 +2112,9 @@ public final class ItemListPeerItemHeader: ListViewItemHeader {
public let strings: PresentationStrings
public let actionTitle: String?
public let action: (() -> Void)?
-
+
public let height: CGFloat = 28.0
-
+
public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) {
self.context = context
self.text = text
@@ -2082,11 +2133,11 @@ public final class ItemListPeerItemHeader: ListViewItemHeader {
return false
}
}
-
+
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, context: self.context, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action)
}
-
+
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
(node as? ItemListPeerItemHeaderNode)?.update(text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action)
}
@@ -2098,9 +2149,9 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
private let context: AccountContext
private var actionTitle: String?
private var action: (() -> Void)?
-
+
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
-
+
private let backgroundNode: ASDisplayNode
private let snappedBackgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
@@ -2108,29 +2159,29 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
private let additionalTextNode: ImmediateTextNode
private let actionTextNode: ImmediateTextNode
private let actionButton: HighlightableButtonNode
-
+
private var stickDistanceFactor: CGFloat?
-
+
public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
self.context = context
self.theme = theme
self.strings = strings
self.actionTitle = actionTitle
self.action = action
-
+
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor
-
+
self.snappedBackgroundNode = ASDisplayNode()
self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor
self.snappedBackgroundNode.alpha = 0.0
-
+
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.separatorNode.alpha = 0.0
-
+
let titleFont = Font.regular(13.0)
-
+
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 1
@@ -2143,22 +2194,22 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
attemptSynchronous: true
)
self.textNode.visibility = true
-
+
self.additionalTextNode = ImmediateTextNode()
self.additionalTextNode.displaysAsynchronously = false
self.additionalTextNode.maximumNumberOfLines = 1
self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
-
+
self.actionTextNode = ImmediateTextNode()
self.actionTextNode.displaysAsynchronously = false
self.actionTextNode.maximumNumberOfLines = 1
self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor)
-
+
self.actionButton = HighlightableButtonNode()
self.actionButton.isUserInteractionEnabled = self.action != nil
-
+
super.init()
-
+
self.addSubnode(self.backgroundNode)
self.addSubnode(self.snappedBackgroundNode)
self.addSubnode(self.separatorNode)
@@ -2166,7 +2217,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
self.addSubnode(self.additionalTextNode)
self.addSubnode(self.actionTextNode)
self.addSubnode(self.actionButton)
-
+
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.actionButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
@@ -2180,25 +2231,25 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
}
}
}
-
+
@objc private func actionButtonPressed() {
self.action?()
}
-
+
public func updateTheme(theme: PresentationTheme) {
self.theme = theme
-
+
self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor
self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor
self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
-
+
let titleFont = Font.regular(13.0)
-
+
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
self.additionalTextNode.attributedText = NSAttributedString(string: self.additionalTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
}
-
+
public func update(text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
self.actionTitle = actionTitle
self.action = action
@@ -2211,31 +2262,31 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
}
}
-
+
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, leftInset, rightInset)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.snappedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
-
+
let sideInset: CGFloat = 15.0 + leftInset
-
+
let actionTextSize = self.actionTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: size.height))
let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - actionTextSize.width - 8.0, height: size.height))
let textSize = self.textNode.updateLayout(CGSize(width: max(1.0, size.width - sideInset * 2.0 - actionTextSize.width - 8.0 - additionalTextSize.width), height: size.height))
-
+
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 7.0), size: textSize)
self.textNode.frame = textFrame
self.additionalTextNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX, y: 7.0), size: additionalTextSize)
self.actionTextNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 7.0), size: actionTextSize)
self.actionButton.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 0.0), size: CGSize(width: actionTextSize.width, height: size.height))
}
-
+
override public func animateRemoved(duration: Double) {
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true)
}
-
+
override public func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) {
if self.stickDistanceFactor == factor {
return
diff --git a/submodules/ItemListUI/Sources/Items/ItemListExpandableSelectionItem.swift b/submodules/ItemListUI/Sources/Items/ItemListExpandableSelectionItem.swift
new file mode 100644
index 0000000000..a90570106a
--- /dev/null
+++ b/submodules/ItemListUI/Sources/Items/ItemListExpandableSelectionItem.swift
@@ -0,0 +1,463 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import SwiftSignalKit
+import TelegramPresentationData
+import AppBundle
+import CheckNode
+
+// An expandable row for options: single-select radio-style or multi-select checkbox-style.
+// The header shows the selected value (single) or a count (multi), and the inline sub-items use
+// square checkbox visuals. In multi-select mode the menu stays open after a tap.
+public class ItemListExpandableSelectionItem: ListViewItem, ItemListItem {
+ public enum SelectionMode {
+ case single
+ case multiple
+ }
+
+ public struct Option: Equatable {
+ public var id: AnyHashable
+ public var title: String
+ public var isSelected: Bool
+ public var index: Int
+
+ public init(id: AnyHashable, title: String, isSelected: Bool, index: Int = 0) {
+ self.id = id
+ self.title = title
+ self.isSelected = isSelected
+ self.index = index
+ }
+ }
+
+ public let presentationData: ItemListPresentationData
+ public let systemStyle: ItemListSystemStyle
+ public let title: String
+ public let options: [Option]
+ public let mode: SelectionMode
+ public let isExpanded: Bool
+ public let sectionId: ItemListSectionId
+ public let style: ItemListStyle
+ public let updated: (Option) -> Void
+ public let toggleExpanded: () -> Void
+ public let tag: ItemListItemTag?
+
+ public var isAlwaysPlain: Bool { return false }
+ public let selectable: Bool = true
+
+ public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, options: [Option], mode: SelectionMode = .single, isExpanded: Bool, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Option) -> Void, toggleExpanded: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) {
+ self.presentationData = presentationData
+ self.systemStyle = systemStyle
+ self.title = title
+ self.options = options
+ self.mode = mode
+ self.isExpanded = isExpanded
+ self.sectionId = sectionId
+ self.style = style
+ self.updated = updated
+ self.toggleExpanded = toggleExpanded
+ self.tag = tag
+ }
+
+ public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
+ async {
+ let node = ItemListExpandableSelectionItemNode()
+ let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ node.contentSize = layout.contentSize
+ node.insets = layout.insets
+ Queue.mainQueue().async {
+ completion(node, {
+ return (nil, { _ in apply(ListViewItemUpdateAnimation.None) })
+ })
+ }
+ }
+ }
+
+ public 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) {
+ Queue.mainQueue().async {
+ if let nodeValue = node() as? ItemListExpandableSelectionItemNode {
+ let makeLayout = nodeValue.asyncLayout()
+ async {
+ let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ Queue.mainQueue().async {
+ completion(layout, { _ in
+ apply(animation)
+ })
+ }
+ }
+ }
+ }
+ }
+
+ public func selected(listView: ListView) {
+ listView.clearHighlightAnimated(true)
+ // Tapping the header row toggles the inline option list open/closed.
+ self.toggleExpanded()
+ }
+}
+
+private final class SelectionOptionNode: HighlightTrackingButtonNode {
+ private let textNode: ImmediateTextNode
+ private var checkNode: CheckNode?
+ private let separatorNode: ASDisplayNode
+
+ private var theme: PresentationTheme?
+ private var option: ItemListExpandableSelectionItem.Option?
+ private var action: ((ItemListExpandableSelectionItem.Option) -> Void)?
+
+ init() {
+ self.textNode = ImmediateTextNode()
+ self.separatorNode = ASDisplayNode()
+ self.separatorNode.isLayerBacked = true
+ super.init()
+ self.addSubnode(self.separatorNode)
+ self.addSubnode(self.textNode)
+ self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
+ }
+
+ @objc private func pressed() {
+ guard let option = self.option, let action = self.action else {
+ return
+ }
+ action(option)
+ }
+
+ func update(presentationData: ItemListPresentationData, option: ItemListExpandableSelectionItem.Option, action: @escaping (ItemListExpandableSelectionItem.Option) -> Void, size: CGSize, transition: ContainedViewLayoutTransition) {
+ let themeUpdated = self.theme !== presentationData.theme
+ self.option = option
+ self.action = action
+ let leftInset: CGFloat = 60.0
+ if themeUpdated {
+ self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
+ }
+ let checkNode: CheckNode
+ if let current = self.checkNode {
+ checkNode = current
+ if themeUpdated {
+ checkNode.theme = CheckNodeTheme(theme: presentationData.theme, style: .plain)
+ }
+ } else {
+ checkNode = CheckNode(theme: CheckNodeTheme(theme: presentationData.theme, style: .plain), content: .check(isRectangle: true))
+ checkNode.isUserInteractionEnabled = false
+ self.checkNode = checkNode
+ self.addSubnode(checkNode)
+ }
+ let checkSize = CGSize(width: 22.0, height: 22.0)
+ checkNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) / 2.0), y: floor((size.height - checkSize.height) / 2.0)), size: checkSize)
+ checkNode.setSelected(option.isSelected, animated: transition.isAnimated)
+ transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: size.height - UIScreenPixel), size: CGSize(width: size.width - leftInset - 16.0, height: UIScreenPixel)))
+ self.textNode.attributedText = NSAttributedString(string: option.title, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
+ let titleSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset, height: 100.0))
+ self.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
+ }
+}
+
+public class ItemListExpandableSelectionItemNode: ListViewItemNode, ItemListItemNode {
+ private let backgroundNode: ASDisplayNode
+ private let topStripeNode: ASDisplayNode
+ private let bottomTopStripeNode: ASDisplayNode
+ private let bottomStripeNode: ASDisplayNode
+ private let highlightedBackgroundNode: ASDisplayNode
+ private let maskNode: ASImageNode
+
+ private let titleNode: TextNode
+ private let titleValueNode: TextNode
+ private let expandArrowNode: ASImageNode
+ private let subItemContainer: ASDisplayNode
+ private var subItemNodes: [AnyHashable: SelectionOptionNode] = [:]
+ private let activateArea: AccessibilityAreaNode
+
+ private var item: ItemListExpandableSelectionItem?
+
+ public var tag: ItemListItemTag? {
+ return self.item?.tag
+ }
+
+ public init() {
+ self.backgroundNode = ASDisplayNode()
+ self.backgroundNode.isLayerBacked = true
+ self.backgroundNode.backgroundColor = .white
+ self.maskNode = ASImageNode()
+ self.maskNode.isUserInteractionEnabled = false
+ self.topStripeNode = ASDisplayNode()
+ self.topStripeNode.isLayerBacked = true
+ self.bottomTopStripeNode = ASDisplayNode()
+ self.bottomTopStripeNode.isLayerBacked = true
+ self.bottomStripeNode = ASDisplayNode()
+ self.bottomStripeNode.isLayerBacked = true
+ self.titleNode = TextNode()
+ self.titleNode.isUserInteractionEnabled = false
+ self.titleValueNode = TextNode()
+ self.titleValueNode.isUserInteractionEnabled = false
+ self.expandArrowNode = ASImageNode()
+ self.expandArrowNode.displaysAsynchronously = false
+ self.highlightedBackgroundNode = ASDisplayNode()
+ self.highlightedBackgroundNode.isLayerBacked = true
+ self.activateArea = AccessibilityAreaNode()
+ self.subItemContainer = ASDisplayNode()
+ self.subItemContainer.clipsToBounds = true
+ super.init(layerBacked: false)
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.titleValueNode)
+ self.addSubnode(self.expandArrowNode)
+ self.addSubnode(self.activateArea)
+ self.addSubnode(self.subItemContainer)
+ self.activateArea.activate = { [weak self] in
+ guard let strongSelf = self, let item = strongSelf.item else {
+ return false
+ }
+ item.toggleExpanded()
+ return true
+ }
+ }
+
+ public func asyncLayout() -> (_ item: ItemListExpandableSelectionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
+ let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
+ let makeTitleValueLayout = TextNode.asyncLayout(self.titleValueNode)
+ let currentItem = self.item
+ return { item, params, neighbors in
+ let separatorHeight = UIScreenPixel
+ let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
+ var updatedTheme: PresentationTheme?
+ if currentItem?.presentationData.theme !== item.presentationData.theme {
+ updatedTheme = item.presentationData.theme
+ }
+ let itemBackgroundColor: UIColor
+ let itemSeparatorColor: UIColor
+ let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
+ var contentSize: CGSize
+ var insets: UIEdgeInsets
+ switch item.style {
+ case .plain:
+ itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
+ itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
+ contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
+ insets = itemListNeighborsPlainInsets(neighbors)
+ case .blocks:
+ itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
+ itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
+ contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
+ insets = itemListNeighborsGroupedInsets(neighbors, params)
+ }
+ let leftInset: CGFloat = 16.0 + params.leftInset
+ let selectedCount = item.options.filter(\.isSelected).count
+ let titleValue: String
+ switch item.mode {
+ case .single:
+ titleValue = item.options.first(where: { $0.isSelected })?.title ?? "\(selectedCount)/\(item.options.count)"
+ case .multiple:
+ titleValue = "\(selectedCount)/\(item.options.count)"
+ }
+ let arrowReservedWidth: CGFloat = 30.0
+ let valueRightInset = params.rightInset + 16.0 + arrowReservedWidth
+ let titleConstrainedWidth = params.width - leftInset - valueRightInset - 12.0
+ let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
+ let valueConstrainedWidth = max(40.0, params.width - leftInset - valueRightInset - titleLayout.size.width - 12.0)
+ let (titleValueLayout, titleValueApply) = makeTitleValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleValue, font: Font.regular(16.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: valueConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
+ let verticalInset: CGFloat = item.systemStyle == .glass ? 15.0 : 11.0
+ contentSize.height = max(contentSize.height, titleLayout.size.height + verticalInset * 2.0)
+ let mainContentHeight = contentSize.height
+ let optionHeight: CGFloat = item.systemStyle == .glass ? 52.0 : 44.0
+ let effectiveSubItemsHeight: CGFloat = item.isExpanded ? CGFloat(item.options.count) * optionHeight : 0.0
+ contentSize.height += effectiveSubItemsHeight
+ let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
+ let layoutSize = layout.size
+ return (layout, { [weak self] animation in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.item = item
+ let transition: ContainedViewLayoutTransition = animation.transition
+ strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: mainContentHeight))
+ strongSelf.activateArea.accessibilityLabel = item.title
+ strongSelf.activateArea.accessibilityValue = titleValue
+ if updatedTheme != nil {
+ strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
+ strongSelf.bottomTopStripeNode.backgroundColor = itemSeparatorColor
+ strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
+ strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
+ strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
+ strongSelf.expandArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: item.presentationData.theme.list.itemPrimaryTextColor)
+ }
+ switch item.style {
+ case .plain:
+ if strongSelf.backgroundNode.supernode != nil {
+ strongSelf.backgroundNode.removeFromSupernode()
+ }
+ if strongSelf.topStripeNode.supernode != nil {
+ strongSelf.topStripeNode.removeFromSupernode()
+ }
+ if strongSelf.bottomStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
+ }
+ if strongSelf.bottomTopStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomTopStripeNode, at: 1)
+ }
+ if strongSelf.maskNode.supernode != nil {
+ strongSelf.maskNode.removeFromSupernode()
+ }
+ strongSelf.bottomTopStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: mainContentHeight - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
+ strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: layout.contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
+ case .blocks:
+ if strongSelf.backgroundNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
+ }
+ if strongSelf.topStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
+ }
+ if strongSelf.bottomStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
+ }
+ if strongSelf.bottomTopStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomTopStripeNode, at: 3)
+ }
+ if strongSelf.maskNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.activateArea)
+ }
+ let hasCorners = itemListHasRoundedBlockLayout(params)
+ var hasTopCorners = false
+ var hasBottomCorners = false
+ switch neighbors.top {
+ case .sameSection(false):
+ strongSelf.topStripeNode.isHidden = true
+ default:
+ hasTopCorners = true
+ strongSelf.topStripeNode.isHidden = hasCorners
+ }
+ let bottomStripeInset: CGFloat
+ switch neighbors.bottom {
+ case .sameSection(false):
+ bottomStripeInset = leftInset
+ strongSelf.bottomStripeNode.isHidden = false
+ strongSelf.bottomTopStripeNode.isHidden = false
+ default:
+ bottomStripeInset = 0.0
+ hasBottomCorners = true
+ strongSelf.bottomStripeNode.isHidden = hasCorners
+ strongSelf.bottomTopStripeNode.isHidden = false
+ }
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
+ let backgroundFrame = 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)))
+ animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
+ animation.animator.updateFrame(layer: strongSelf.maskNode.layer, frame: backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0), completion: nil)
+ animation.animator.updateFrame(layer: strongSelf.topStripeNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)), completion: nil)
+ animation.animator.updateFrame(layer: strongSelf.bottomTopStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: mainContentHeight - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)), completion: nil)
+ animation.animator.updateFrame(layer: strongSelf.bottomStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)), completion: nil)
+ }
+ let _ = titleApply()
+ strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((mainContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
+ let _ = titleValueApply()
+ let valueX = params.width - valueRightInset - titleValueLayout.size.width
+ strongSelf.titleValueNode.frame = CGRect(origin: CGPoint(x: max(strongSelf.titleNode.frame.maxX + 9.0, valueX), y: strongSelf.titleNode.frame.minY + floor((titleLayout.size.height - titleValueLayout.size.height) / 2.0)), size: titleValueLayout.size)
+ if let image = strongSelf.expandArrowNode.image {
+ strongSelf.expandArrowNode.position = CGPoint(x: params.width - params.rightInset - 16.0 - image.size.width * 0.4, y: strongSelf.titleValueNode.frame.midY)
+ let scaleFactor: CGFloat = 0.8
+ strongSelf.expandArrowNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor))
+ transition.updateTransformRotation(node: strongSelf.expandArrowNode, angle: item.isExpanded ? CGFloat.pi * -0.5 : CGFloat.pi * 0.5)
+ }
+ strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: optionHeight + UIScreenPixel + UIScreenPixel))
+ animation.animator.updateFrame(layer: strongSelf.subItemContainer.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: mainContentHeight), size: CGSize(width: params.width, height: effectiveSubItemsHeight)), completion: nil)
+ var validIds: [AnyHashable] = []
+ let subItemSize = CGSize(width: params.width - params.leftInset - params.rightInset, height: optionHeight)
+ var nextSubItemPosition = CGPoint(x: params.leftInset, y: 0.0)
+ for option in item.options {
+ validIds.append(option.id)
+ let subItemNode: SelectionOptionNode
+ var subItemNodeTransition = animation.transition
+ if let current = strongSelf.subItemNodes[option.id] {
+ subItemNode = current
+ } else {
+ subItemNodeTransition = .immediate
+ subItemNode = SelectionOptionNode()
+ strongSelf.subItemNodes[option.id] = subItemNode
+ strongSelf.subItemContainer.addSubnode(subItemNode)
+ }
+ let subItemFrame = CGRect(origin: nextSubItemPosition, size: subItemSize)
+ subItemNode.update(presentationData: item.presentationData, option: option, action: { option in
+ item.updated(option)
+ if item.mode == .single {
+ item.toggleExpanded()
+ }
+ }, size: subItemSize, transition: subItemNodeTransition)
+ subItemNodeTransition.updateFrame(node: subItemNode, frame: subItemFrame)
+ nextSubItemPosition.y += subItemSize.height
+ }
+ var removeIds: [AnyHashable] = []
+ for (id, itemNode) in strongSelf.subItemNodes {
+ if !validIds.contains(id) {
+ removeIds.append(id)
+ itemNode.removeFromSupernode()
+ }
+ }
+ for id in removeIds {
+ strongSelf.subItemNodes.removeValue(forKey: id)
+ }
+ })
+ }
+ }
+
+ override public func accessibilityActivate() -> Bool {
+ return false
+ }
+
+ override public func visibleForSelection(at point: CGPoint) -> Bool {
+ if !self.canBeSelected {
+ return false
+ }
+ if point.y > self.subItemContainer.frame.minY {
+ return false
+ }
+ return true
+ }
+
+ override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
+ var highlighted = highlighted
+ if point.y > self.subItemContainer.frame.minY {
+ highlighted = false
+ }
+ super.setHighlighted(highlighted, at: point, animated: animated)
+ if highlighted {
+ self.highlightedBackgroundNode.alpha = 1.0
+ if self.highlightedBackgroundNode.supernode == nil {
+ var anchorNode: ASDisplayNode?
+ if self.bottomStripeNode.supernode != nil {
+ anchorNode = self.bottomStripeNode
+ } else if self.topStripeNode.supernode != nil {
+ anchorNode = self.topStripeNode
+ } else if self.backgroundNode.supernode != nil {
+ anchorNode = self.backgroundNode
+ }
+ if let anchorNode = anchorNode {
+ self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
+ } else {
+ self.addSubnode(self.highlightedBackgroundNode)
+ }
+ }
+ } else {
+ if self.highlightedBackgroundNode.supernode != nil {
+ if animated {
+ self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
+ if let strongSelf = self, completed {
+ strongSelf.highlightedBackgroundNode.removeFromSupernode()
+ }
+ })
+ self.highlightedBackgroundNode.alpha = 0.0
+ } else {
+ self.highlightedBackgroundNode.removeFromSupernode()
+ }
+ }
+ }
+ }
+
+ override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
+ self.layer.allowsGroupOpacity = true
+ self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in
+ self?.layer.allowsGroupOpacity = false
+ })
+ }
+
+ override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
+ self.layer.allowsGroupOpacity = true
+ self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
+ }
+}
diff --git a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift
index 6f95c58d2c..3855d36af8 100644
--- a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift
+++ b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift
@@ -19,7 +19,7 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
public var title: String
public var isSelected: Bool
public var isEnabled: Bool
-
+
public init(
id: AnyHashable,
title: String,
@@ -32,7 +32,7 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
self.isEnabled = isEnabled
}
}
-
+
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
@@ -54,9 +54,9 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
let selectAction: () -> Void
let subAction: (SubItem) -> Void
public let tag: ItemListItemTag?
-
+
public let selectable: Bool = true
-
+
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, title: String, value: Bool, isExpanded: Bool, subItems: [SubItem], type: ItemListExpandableSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, selectAction: @escaping () -> Void, subAction: @escaping (SubItem) -> Void, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
@@ -80,15 +80,15 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
self.subAction = subAction
self.tag = tag
}
-
+
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListExpandableSwitchItemNode(type: self.type)
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
-
+
node.contentSize = layout.contentSize
node.insets = layout.insets
-
+
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(ListViewItemUpdateAnimation.None) })
@@ -96,18 +96,18 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
}
}
}
-
+
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
-
+
self.selectAction()
}
-
+
public 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) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListExpandableSwitchItemNode {
let makeLayout = nodeValue.asyncLayout()
-
+
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
@@ -125,45 +125,45 @@ private final class SubItemNode: HighlightTrackingButtonNode {
private let textNode: ImmediateTextNode
private var checkNode: CheckNode?
private let separatorNode: ASDisplayNode
-
+
private var theme: PresentationTheme?
private var item: ItemListExpandableSwitchItem.SubItem?
private var action: ((ItemListExpandableSwitchItem.SubItem) -> Void)?
-
+
init() {
self.textNode = ImmediateTextNode()
-
+
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
-
+
super.init()
-
+
self.addSubnode(self.separatorNode)
self.addSubnode(self.textNode)
-
+
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
-
+
@objc private func pressed() {
guard let item = self.item, item.isEnabled, let action = self.action else {
return
}
action(item)
}
-
+
func update(presentationData: ItemListPresentationData, item: ItemListExpandableSwitchItem.SubItem, action: @escaping (ItemListExpandableSwitchItem.SubItem) -> Void, size: CGSize, transition: ContainedViewLayoutTransition) {
let themeUpdated = self.theme !== presentationData.theme
-
+
self.item = item
self.action = action
-
+
let leftInset: CGFloat = 60.0
let separatorRightInset: CGFloat = 16.0
-
+
if themeUpdated {
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
}
-
+
let checkNode: CheckNode
if let current = self.checkNode {
checkNode = current
@@ -171,23 +171,23 @@ private final class SubItemNode: HighlightTrackingButtonNode {
checkNode.theme = CheckNodeTheme(theme: presentationData.theme, style: .plain)
}
} else {
- checkNode = CheckNode(theme: CheckNodeTheme(theme: presentationData.theme, style: .plain))
+ checkNode = CheckNode(theme: CheckNodeTheme(theme: presentationData.theme, style: .plain), content: .check(isRectangle: true))
checkNode.isUserInteractionEnabled = false
self.checkNode = checkNode
self.addSubnode(checkNode)
}
-
+
let checkSize = CGSize(width: 22.0, height: 22.0)
checkNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) / 2.0), y: floor((size.height - checkSize.height) / 2.0)), size: checkSize)
-
+
checkNode.setSelected(item.isSelected, animated: transition.isAnimated)
-
+
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: size.height - UIScreenPixel), size: CGSize(width: size.width - leftInset - separatorRightInset, height: UIScreenPixel)))
-
+
self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let titleSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset, height: 100.0))
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
-
+
self.alpha = item.isEnabled ? 1.0 : 0.5
}
}
@@ -199,7 +199,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
-
+
private let iconNode: ASImageNode
private let titleNode: TextNode
private let titleValueNode: TextNode
@@ -207,41 +207,41 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
-
+
private var lockedIconNode: ASImageNode?
-
+
private let subItemContainer: ASDisplayNode
private var subItemNodes: [AnyHashable: SubItemNode] = [:]
-
+
private let activateArea: AccessibilityAreaNode
-
+
private var item: ItemListExpandableSwitchItem?
-
+
public var tag: ItemListItemTag? {
return self.item?.tag
}
-
+
public init(type: ItemListExpandableSwitchItemNodeType) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
-
+
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
-
+
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
-
+
self.bottomTopStripeNode = ASDisplayNode()
self.bottomTopStripeNode.isLayerBacked = true
-
+
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
-
+
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
-
+
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
switch type {
@@ -250,25 +250,25 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
case .icon:
self.switchNode = IconSwitchNode()
}
-
+
self.titleValueNode = TextNode()
self.titleValueNode.isUserInteractionEnabled = false
-
+
self.expandArrowNode = ASImageNode()
self.expandArrowNode.displaysAsynchronously = false
-
+
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
-
+
self.switchGestureNode = ASDisplayNode()
-
+
self.activateArea = AccessibilityAreaNode()
-
+
self.subItemContainer = ASDisplayNode()
self.subItemContainer.clipsToBounds = true
-
+
super.init(layerBacked: false)
-
+
self.addSubnode(self.titleNode)
self.addSubnode(self.titleValueNode)
self.addSubnode(self.expandArrowNode)
@@ -276,7 +276,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
self.addSubnode(self.subItemContainer)
-
+
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
return false
@@ -289,42 +289,42 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
return true
}
}
-
+
override public func didLoad() {
super.didLoad()
-
+
(self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged)
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
-
+
func asyncLayout() -> (_ item: ItemListExpandableSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTitleValueLayout = TextNode.asyncLayout(self.titleValueNode)
-
+
let currentItem = self.item
var currentDisabledOverlayNode = self.disabledOverlayNode
-
+
return { item, params, neighbors in
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
-
+
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
-
+
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
-
+
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
-
+
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
-
+
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
@@ -337,22 +337,22 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
-
+
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
-
+
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
-
+
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0 - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
let titleValue = "\(item.subItems.filter(\.isSelected).count)/\(item.subItems.count)"
let (titleValueLayout, titleValueApply) = makeTitleValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleValue, font: Font.bold(14.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0 - titleLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
@@ -360,16 +360,16 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
case .legacy:
verticalInset = 11.0
}
-
+
contentSize.height = max(contentSize.height, titleLayout.size.height + verticalInset * 2.0)
-
+
let mainContentHeight = contentSize.height
var effectiveSubItemsHeight: CGFloat = 0.0
if item.isExpanded {
effectiveSubItemsHeight = CGFloat(item.subItems.count) * (item.systemStyle == .glass ? 52.0 : 44.0)
}
contentSize.height += effectiveSubItemsHeight
-
+
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
@@ -377,16 +377,16 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
} else {
currentDisabledOverlayNode = nil
}
-
+
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
-
+
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animation in
if let strongSelf = self {
strongSelf.item = item
-
+
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: mainContentHeight))
-
+
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off
strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint
@@ -396,7 +396,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
accessibilityTraits.insert(.notEnabled)
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
-
+
if let icon = item.icon {
if strongSelf.iconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconNode)
@@ -410,9 +410,9 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
-
+
let transition: ContainedViewLayoutTransition = animation.transition
-
+
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
@@ -430,24 +430,24 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
})
strongSelf.disabledOverlayNode = nil
}
-
+
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomTopStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
-
+
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
-
+
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
-
+
let _ = titleApply()
-
+
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
@@ -483,7 +483,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode)
}
-
+
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
@@ -506,9 +506,9 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
strongSelf.bottomStripeNode.isHidden = hasCorners
strongSelf.bottomTopStripeNode.isHidden = false
}
-
+
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
-
+
let backgroundFrame = 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)))
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
animation.animator.updateFrame(layer: strongSelf.maskNode.layer, frame: backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0), completion: nil)
@@ -516,12 +516,12 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
animation.animator.updateFrame(layer: strongSelf.bottomTopStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: mainContentHeight - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)), completion: nil)
animation.animator.updateFrame(layer: strongSelf.bottomStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)), completion: nil)
}
-
+
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((mainContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
-
+
let _ = titleValueApply()
strongSelf.titleValueNode.frame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 9.0, y: strongSelf.titleNode.frame.minY + floor((titleLayout.size.height - titleValueLayout.size.height) / 2.0)), size: titleValueLayout.size)
-
+
if let updatedTheme {
strongSelf.expandArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: updatedTheme.list.itemPrimaryTextColor)
}
@@ -531,13 +531,13 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
strongSelf.expandArrowNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor))
transition.updateTransformRotation(node: strongSelf.expandArrowNode, angle: item.isExpanded ? CGFloat.pi * -0.5 : CGFloat.pi * 0.5)
}
-
+
if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
-
+
strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((mainContentHeight - switchSize.height) / 2.0)), size: switchSize)
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
if switchView.isOn != item.value {
@@ -546,13 +546,13 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
switchView.isUserInteractionEnabled = item.enableInteractiveChanges
}
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
-
+
if item.displayLocked {
var updateLockedIconImage = false
if let _ = updatedTheme {
updateLockedIconImage = true
}
-
+
let lockedIconNode: ASImageNode
if let current = strongSelf.lockedIconNode {
lockedIconNode = current
@@ -562,13 +562,13 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
strongSelf.lockedIconNode = lockedIconNode
strongSelf.insertSubnode(lockedIconNode, aboveSubnode: strongSelf.switchNode)
}
-
+
if updateLockedIconImage, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: item.presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
-
+
let switchFrame = strongSelf.switchNode.frame
-
+
if let icon = lockedIconNode.image {
lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size)
}
@@ -576,17 +576,17 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
strongSelf.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
-
+
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: (item.systemStyle == .glass ? 52.0 : 44.0) + UIScreenPixel + UIScreenPixel))
-
+
animation.animator.updateFrame(layer: strongSelf.subItemContainer.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: mainContentHeight), size: CGSize(width: params.width, height: effectiveSubItemsHeight)), completion: nil)
-
+
var validIds: [AnyHashable] = []
let subItemSize = CGSize(width: params.width - params.leftInset - params.rightInset, height: item.systemStyle == .glass ? 52.0 : 44.0)
var nextSubItemPosition = CGPoint(x: params.leftInset, y: 0.0)
for subItem in item.subItems {
validIds.append(subItem.id)
-
+
let subItemNode: SubItemNode
var subItemNodeTransition = transition
if let current = strongSelf.subItemNodes[subItem.id] {
@@ -600,7 +600,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
let subItemFrame = CGRect(origin: nextSubItemPosition, size: subItemSize)
subItemNode.update(presentationData: item.presentationData, item: subItem, action: item.subAction, size: subItemSize, transition: subItemNodeTransition)
subItemNodeTransition.updateFrame(node: subItemNode, frame: subItemFrame)
-
+
nextSubItemPosition.y += subItemSize.height
}
var removeIds: [AnyHashable] = []
@@ -617,7 +617,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
})
}
}
-
+
override public func accessibilityActivate() -> Bool {
guard let item = self.item else {
return false
@@ -634,7 +634,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
}
return true
}
-
+
override public func visibleForSelection(at point: CGPoint) -> Bool {
if !self.canBeSelected {
return false
@@ -642,18 +642,18 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
if point.y > self.subItemContainer.frame.minY {
return false
}
-
+
return true
}
-
+
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
var highlighted = highlighted
if point.y > self.subItemContainer.frame.minY {
highlighted = false
}
-
+
super.setHighlighted(highlighted, at: point, animated: animated)
-
+
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
@@ -688,26 +688,26 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod
}
}
}
-
+
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in
self?.layer.allowsGroupOpacity = false
})
}
-
+
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
-
+
@objc private func switchValueChanged(_ switchView: UISwitch) {
if let item = self.item {
let value = switchView.isOn
item.updated(value)
}
}
-
+
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
if item.enabled {
diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift
index 3a05467b15..ec7af3f073 100644
--- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift
+++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift
@@ -18,7 +18,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
case primary
case accent
}
-
+
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
@@ -40,7 +40,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
let activatedWhileDisabled: () -> Void
let action: (() -> Void)?
public let tag: ItemListItemTag?
-
+
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, title: String, text: String? = nil, textColor: TextColor = .primary, titleBadgeComponent: AnyComponent? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
@@ -64,15 +64,15 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
self.action = action
self.tag = tag
}
-
+
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListSwitchItemNode(type: self.type)
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
-
+
node.contentSize = layout.contentSize
node.insets = layout.insets
-
+
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
@@ -80,12 +80,12 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
}
}
}
-
+
public 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) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListSwitchItemNode {
let makeLayout = nodeValue.asyncLayout()
-
+
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
@@ -101,11 +101,11 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
}
}
}
-
+
public var selectable: Bool {
return self.action != nil && self.enabled
}
-
+
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
@@ -120,7 +120,7 @@ protocol ItemListSwitchNodeImpl {
var handleColor: UIColor { get set }
var positiveContentColor: UIColor { get set }
var negativeContentColor: UIColor { get set }
-
+
var isOn: Bool { get }
func setOn(_ value: Bool, animated: Bool)
}
@@ -130,14 +130,14 @@ extension SwitchNode: ItemListSwitchNodeImpl {
get {
return .white
} set(value) {
-
+
}
}
var negativeContentColor: UIColor {
get {
return .white
} set(value) {
-
+
}
}
}
@@ -152,22 +152,22 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
-
+
private let iconNode: ASImageNode
private let titleNode: TextNode
private var textNode: TextNode?
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
-
+
private var titleBadgeComponentView: ComponentView?
-
+
private var lockedIconNode: ASImageNode?
-
+
private let activateArea: AccessibilityAreaNode
-
+
private var item: ItemListSwitchItem?
-
+
public var tag: ItemListItemTag? {
return self.item?.tag
}
@@ -175,7 +175,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
public var contextSourceView: UIView {
return self.textNode?.view ?? self.switchNode.view
}
-
+
public init(type: ItemListSwitchItemNodeType) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
@@ -183,45 +183,45 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
-
+
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
-
+
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
-
+
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
-
+
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
-
+
self.titleNode = TextNode()
self.titleNode.anchorPoint = CGPoint()
self.titleNode.isUserInteractionEnabled = false
-
+
switch type {
case .regular:
self.switchNode = SwitchNode()
case .icon:
self.switchNode = IconSwitchNode()
}
-
+
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
-
+
self.switchGestureNode = ASDisplayNode()
-
+
self.activateArea = AccessibilityAreaNode()
-
+
super.init(layerBacked: false)
-
+
self.addSubnode(self.titleNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
-
+
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
return false
@@ -234,21 +234,21 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
return true
}
}
-
+
override public func didLoad() {
super.didLoad()
-
+
(self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged)
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
-
+
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
-
+
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
@@ -269,41 +269,41 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
transition.updateAlpha(node: self.switchNode, alpha: hasContextMenu ? 0.5 : 1.0)
}
}
-
+
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
-
+
let currentItem = self.item
var currentDisabledOverlayNode = self.disabledOverlayNode
-
+
return { item, params, neighbors in
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
-
+
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
-
+
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
-
+
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
-
+
var updatedValue = false
if currentItem?.value != item.value {
updatedValue = true
}
-
+
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
-
+
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
@@ -316,9 +316,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
contentSize = CGSize(width: params.width, height: 44.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
-
-
-
+
+
+
var topInset: CGFloat
if item.text != nil {
topInset = 9.0
@@ -329,23 +329,23 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
contentSize.height = 52.0
topInset += 4.0
}
-
+
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
-
+
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
-
+
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
-
+
contentSize.height = max(contentSize.height, titleLayout.size.height + topInset * 2.0)
-
+
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
-
+
if let text = item.text {
let textColor: UIColor
switch item.textColor {
@@ -358,7 +358,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
contentSize.height += -1.0 + textLayout.size.height
textLayoutAndApply = (textLayout, textApply)
}
-
+
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
@@ -366,10 +366,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} else {
currentDisabledOverlayNode = nil
}
-
+
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
-
+
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
@@ -378,11 +378,11 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} else {
transition = .immediate
}
-
+
strongSelf.item = item
-
+
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
-
+
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off
strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint
@@ -392,7 +392,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
accessibilityTraits.insert(.notEnabled)
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
-
+
if let icon = item.icon {
var iconTransition = transition
if strongSelf.iconNode.supernode == nil {
@@ -413,7 +413,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
-
+
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
@@ -431,7 +431,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
})
strongSelf.disabledOverlayNode = nil
}
-
+
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
@@ -444,9 +444,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor
}
-
+
let _ = titleApply()
-
+
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
@@ -475,7 +475,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode)
}
-
+
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
@@ -496,16 +496,16 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
-
+
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
-
+
transition.updateFrame(node: 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))))
transition.updateFrame(node: strongSelf.highlightNode, 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))))
transition.updateFrame(node: strongSelf.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0))
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)))
}
-
+
var titleOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0
if textLayoutAndApply != nil {
titleOriginY = topInset + 1.0
@@ -513,7 +513,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleOriginY), size: titleLayout.size)
transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin)
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
-
+
if let (textLayout, textApply) = textLayoutAndApply {
let textNode = textApply()
if strongSelf.textNode !== textNode {
@@ -521,7 +521,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.textNode = textNode
textNode.isUserInteractionEnabled = false
strongSelf.addSubnode(textNode)
-
+
if transition.isAnimated {
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
@@ -537,22 +537,29 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
textNode.removeFromSupernode()
}
}
-
+
if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
-
- transition.updateFrame(node: strongSelf.switchNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize))
- strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
+ let switchScale: CGFloat = 1.15
+ let scaledSwitchSize = CGSize(width: switchSize.width * switchScale, height: switchSize.height * switchScale)
+ let switchOrigin = CGPoint(
+ x: params.width - params.rightInset - scaledSwitchSize.width - 15.0 + (scaledSwitchSize.width - switchSize.width) / 2.0,
+ y: floor((contentSize.height - scaledSwitchSize.height) / 2.0 + (scaledSwitchSize.height - switchSize.height) / 2.0)
+ )
+
+ transition.updateFrame(node: strongSelf.switchNode, frame: CGRect(origin: switchOrigin, size: switchSize))
+ strongSelf.switchNode.transform = CATransform3DMakeScale(switchScale, switchScale, 1.0)
+ strongSelf.switchGestureNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - scaledSwitchSize.width - 15.0, y: floor((contentSize.height - scaledSwitchSize.height) / 2.0)), size: scaledSwitchSize)
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animated)
}
switchView.isUserInteractionEnabled = item.enableInteractiveChanges
}
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
-
+
if item.displayLocked {
var lockedIconTransition = transition
var updateLockedIconImage = false
@@ -562,7 +569,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if updatedValue {
updateLockedIconImage = true
}
-
+
let lockedIconNode: ASImageNode
if let current = strongSelf.lockedIconNode {
lockedIconNode = current
@@ -573,13 +580,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.lockedIconNode = lockedIconNode
strongSelf.insertSubnode(lockedIconNode, aboveSubnode: strongSelf.switchNode)
}
-
+
if updateLockedIconImage, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: item.value ? item.presentationData.theme.list.itemSwitchColors.positiveColor : item.presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
-
+
let switchFrame = strongSelf.switchNode.frame
-
+
if let icon = lockedIconNode.image {
let iconOrigin: CGPoint
switch item.systemStyle {
@@ -590,7 +597,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} else {
offset = CGPoint(x: 5.0, y: 1.0)
}
-
+
iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 16.0 + UIScreenPixel + offset.x : switchFrame.minX + 16.0 - UIScreenPixel - offset.x, y: switchFrame.minY + 8.0 + offset.y)
case .legacy:
iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 11.0 : switchFrame.minX + 11.0, y: switchFrame.minY + 9.0)
@@ -601,7 +608,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
-
+
if let component = item.titleBadgeComponent {
let componentView: ComponentView
if let current = strongSelf.titleBadgeComponentView {
@@ -610,7 +617,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
componentView = ComponentView()
strongSelf.titleBadgeComponentView = componentView
}
-
+
let badgeSize = componentView.update(
transition: .immediate,
component: component,
@@ -629,13 +636,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.titleBadgeComponentView = nil
componentView.view?.removeFromSuperview()
}
-
+
transition.updateFrame(node: strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layoutSize.height + UIScreenPixel + UIScreenPixel)))
}
})
}
}
-
+
override public func accessibilityActivate() -> Bool {
guard let item = self.item else {
return false
@@ -652,10 +659,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
}
return true
}
-
+
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
-
+
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
@@ -690,26 +697,26 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
}
}
}
-
+
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in
self?.layer.allowsGroupOpacity = false
})
}
-
+
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
-
+
@objc private func switchValueChanged(_ switchView: UISwitch) {
if let item = self.item {
let value = switchView.isOn
item.updated(value)
}
}
-
+
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
if item.enabled && !item.displayLocked {
diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m
index 36da88228b..82ba8ed37d 100644
--- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m
+++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m
@@ -27,7 +27,7 @@ static NSData * _Nullable parseHexString(NSString * _Nonnull hex) {
return nil;
}
}
-
+
return [NSData dataWithBytesNoCopy:bytes length:[hex length]/2 freeWhenDone:YES];
}
@@ -36,14 +36,14 @@ static NSString * _Nonnull dataToHexString(NSData * _Nonnull data) {
if (dataBuffer == NULL) {
return @"";
}
-
+
NSUInteger dataLength = [data length];
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
-
+
for (int i = 0; i < (int)dataLength; ++i) {
[hexString appendString:[NSString stringWithFormat:@"%02lx", (unsigned long)dataBuffer[i]]];
}
-
+
return hexString;
}
@@ -91,7 +91,7 @@ static NSData *base64_decode(NSString *str) {
while (finalString.length % 4 != 0) {
finalString = [finalString stringByAppendingString:@"="];
}
-
+
hexData = base64_decode(finalString);
}
if (hexData != nil) {
@@ -105,10 +105,10 @@ static NSData *base64_decode(NSString *str) {
if (data == nil || data.length < 16) {
return nil;
}
-
+
uint8_t firstByte = 0;
[data getBytes:&firstByte length:1];
-
+
if (data.length == 16) {
return [[MTProxySecretType0 alloc] initWithSecret:data];
} else if (data.length == 17) {
@@ -359,7 +359,7 @@ static NSData *base64_decode(NSString *str) {
self = [self initWithDeviceModelName:nil];
if (self != nil)
{
-
+
}
return self;
}
@@ -380,7 +380,7 @@ static NSData *base64_decode(NSString *str) {
NSProcessInfo *pInfo = [NSProcessInfo processInfo];
_systemVersion = [[[pInfo operatingSystemVersionString] componentsSeparatedByString:@" "] objectAtIndex:1];
#endif
-
+
NSString *suffix = @"";
#if TARGET_OS_OSX
NSString *value = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"SOURCE"];
@@ -388,11 +388,16 @@ NSString *suffix = @"";
suffix = [NSString stringWithFormat:@"%@", value];
}
#endif
-
+
//SOURCE
NSString *versionString = [[NSString alloc] initWithFormat:@"%@ (%@) %@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"], suffix];
+ // WinterGram: session app-version spoof (bridged from settings via UserDefaults, restart-applied).
+ NSString *wntSpoofAppVersion = [[NSUserDefaults standardUserDefaults] stringForKey:@"wnt_spoofAppVersion"];
+ if (wntSpoofAppVersion != nil && wntSpoofAppVersion.length != 0) {
+ versionString = wntSpoofAppVersion;
+ }
_appVersion = versionString;
-
+
_systemLangCode = [[NSLocale preferredLanguages] objectAtIndex:0];
#if TARGET_OS_OSX
_langPack = @"macos";
@@ -400,7 +405,7 @@ NSString *suffix = @"";
_langPack = @"ios";
#endif
_langPackCode = @"";
-
+
[self _updateApiInitializationHash];
}
return self;
@@ -412,25 +417,25 @@ NSString *suffix = @"";
- (void)setLayer:(NSNumber *)layer {
_layer = layer;
-
+
[self _updateApiInitializationHash];
}
- (void)setAppVersion:(NSString *)appVersion {
_appVersion = appVersion;
-
+
[self _updateApiInitializationHash];
}
- (void)setLangPack:(NSString *)langPack {
_langPack = langPack;
-
+
[self _updateApiInitializationHash];
}
- (void)setLangPackCode:(NSString *)langPackCode {
_langPackCode = langPackCode;
-
+
[self _updateApiInitializationHash];
}
@@ -438,7 +443,7 @@ NSString *suffix = @"";
{
#if TARGET_OS_IPHONE
NSString *platform = [self platform];
-
+
if ([platform isEqualToString:@"iPhone1,1"])
return @"iPhone";
if ([platform isEqualToString:@"iPhone1,2"])
@@ -549,7 +554,7 @@ NSString *suffix = @"";
return @"iPhone 17 Pro Max";
if ([platform isEqualToString:@"iPhone18,4"])
return @"iPhone Air";
-
+
if ([platform hasPrefix:@"iPod1"])
return @"iPod touch 1G";
if ([platform hasPrefix:@"iPod2"])
@@ -564,204 +569,204 @@ NSString *suffix = @"";
return @"iPod touch 6G";
if ([platform hasPrefix:@"iPod9"])
return @"iPod touch 7G";
-
+
if ([platform isEqualToString:@"iPad2,5"] ||
[platform isEqualToString:@"iPad2,6"] ||
[platform isEqualToString:@"iPad2,7"])
return @"iPad mini";
-
+
if ([platform hasPrefix:@"iPad2"])
return @"iPad 2G";
-
+
if ([platform isEqualToString:@"iPad3,1"] ||
[platform isEqualToString:@"iPad3,2"] ||
[platform isEqualToString:@"iPad3,3"])
return @"iPad 3G";
-
+
if ([platform isEqualToString:@"iPad3,4"] ||
[platform isEqualToString:@"iPad3,5"] ||
[platform isEqualToString:@"iPad3,6"])
return @"iPad 3G";
-
+
if ([platform isEqualToString:@"iPad4,1"] ||
[platform isEqualToString:@"iPad4,2"])
return @"iPad Air";
-
+
if ([platform isEqualToString:@"iPad4,4"] ||
[platform isEqualToString:@"iPad4,5"] ||
[platform isEqualToString:@"iPad4,6"])
return @"iPad mini Retina";
-
+
if ([platform isEqualToString:@"iPad4,7"] ||
[platform isEqualToString:@"iPad4,8"] ||
[platform isEqualToString:@"iPad4,9"])
return @"iPad mini 3";
-
+
if ([platform isEqualToString:@"iPad5,1"] ||
[platform isEqualToString:@"iPad5,2"])
return @"iPad mini 4";
-
+
if ([platform isEqualToString:@"iPad5,3"] ||
[platform isEqualToString:@"iPad5,4"])
return @"iPad Air 2";
-
+
if ([platform isEqualToString:@"iPad6,3"] ||
[platform isEqualToString:@"iPad6,4"])
return @"iPad Pro 9.7 inch";
-
+
if ([platform isEqualToString:@"iPad6,7"] ||
[platform isEqualToString:@"iPad6,8"])
return @"iPad Pro 12.9 inch";
-
+
if ([platform isEqualToString:@"iPad6,11"] ||
[platform isEqualToString:@"iPad6,12"])
return @"iPad (2017)";
-
+
if ([platform isEqualToString:@"iPad7,1"] ||
[platform isEqualToString:@"iPad7,2"])
return @"iPad Pro (2nd gen)";
-
+
if ([platform isEqualToString:@"iPad7,3"] ||
[platform isEqualToString:@"iPad7,4"])
return @"iPad Pro 10.5 inch";
-
+
if ([platform isEqualToString:@"iPad7,5"] ||
[platform isEqualToString:@"iPad7,6"])
return @"iPad (6th gen)";
-
+
if ([platform isEqualToString:@"iPad7,11"] ||
[platform isEqualToString:@"iPad7,12"])
return @"iPad 10.2 inch (7th gen)";
-
+
if ([platform isEqualToString:@"iPad8,1"] ||
[platform isEqualToString:@"iPad8,2"] ||
[platform isEqualToString:@"iPad8,3"] ||
[platform isEqualToString:@"iPad8,4"])
return @"iPad Pro 11 inch";
-
+
if ([platform isEqualToString:@"iPad8,5"] ||
[platform isEqualToString:@"iPad8,6"] ||
[platform isEqualToString:@"iPad8,7"] ||
[platform isEqualToString:@"iPad8,8"])
return @"iPad Pro 12.9 inch (3rd gen)";
-
+
if ([platform isEqualToString:@"iPad8,9"] ||
[platform isEqualToString:@"iPad8,10"])
return @"iPad Pro 11 inch (2th gen)";
-
+
if ([platform isEqualToString:@"iPad8,11"] ||
[platform isEqualToString:@"iPad8,12"])
return @"iPad Pro 12.9 inch (4th gen)";
-
+
if ([platform isEqualToString:@"iPad11,1"] ||
[platform isEqualToString:@"iPad11,2"])
return @"iPad mini (5th gen)";
-
+
if ([platform isEqualToString:@"iPad11,3"] ||
[platform isEqualToString:@"iPad11,4"])
return @"iPad Air (3rd gen)";
-
+
if ([platform isEqualToString:@"iPad11,6"] ||
[platform isEqualToString:@"iPad11,7"])
return @"iPad (8th gen)";
-
+
if ([platform isEqualToString:@"iPad12,1"] ||
[platform isEqualToString:@"iPad12,2"])
return @"iPad (9th gen)";
-
+
if ([platform isEqualToString:@"iPad13,1"] ||
[platform isEqualToString:@"iPad13,2"])
return @"iPad Air (4th gen)";
-
+
if ([platform isEqualToString:@"iPad13,4"] ||
[platform isEqualToString:@"iPad13,5"] ||
[platform isEqualToString:@"iPad13,6"] ||
[platform isEqualToString:@"iPad13,7"])
return @"iPad Pro 11 inch (3th gen)";
-
+
if ([platform isEqualToString:@"iPad13,8"] ||
[platform isEqualToString:@"iPad13,9"] ||
[platform isEqualToString:@"iPad13,10"] ||
[platform isEqualToString:@"iPad13,11"])
return @"iPad Pro 12.9 inch (5th gen)";
-
+
if ([platform isEqualToString:@"iPad13,16"] ||
[platform isEqualToString:@"iPad13,17"])
return @"iPad Air (5th gen)";
-
+
if ([platform isEqualToString:@"iPad13,18"] ||
[platform isEqualToString:@"iPad13,19"])
return @"iPad (10th gen)";
-
+
if ([platform isEqualToString:@"iPad14,1"] ||
[platform isEqualToString:@"iPad14,2"])
return @"iPad mini (6th gen)";
-
+
if ([platform isEqualToString:@"iPad14,3"] ||
[platform isEqualToString:@"iPad14,4"])
return @"iPad Pro 11 inch (4th gen)";
-
+
if ([platform isEqualToString:@"iPad14,5"] ||
[platform isEqualToString:@"iPad14,6"])
return @"iPad Pro 12.9 inch (6th gen)";
-
+
if ([platform isEqualToString:@"iPad14,8"] ||
[platform isEqualToString:@"iPad14,9"])
return @"iPad Air (6th gen)";
-
+
if ([platform isEqualToString:@"iPad14,10"] ||
[platform isEqualToString:@"iPad14,11"])
return @"iPad Air (7th gen)";
-
+
if ([platform isEqualToString:@"iPad15,3"] ||
[platform isEqualToString:@"iPad15,4"])
return @"iPad Air 11 inch (7th gen)";
-
+
if ([platform isEqualToString:@"iPad15,5"] ||
[platform isEqualToString:@"iPad15,6"])
return @"iPad Air 13 inch (7th gen)";
-
+
if ([platform isEqualToString:@"iPad15,7"] ||
[platform isEqualToString:@"iPad15,8"])
return @"iPad (11th gen)";
-
+
if ([platform isEqualToString:@"iPad16,1"] ||
[platform isEqualToString:@"iPad16,2"])
return @"iPad mini (7th gen)";
-
+
if ([platform isEqualToString:@"iPad16,3"] ||
[platform isEqualToString:@"iPad16,4"])
return @"iPad Pro 11 inch (5th gen)";
-
+
if ([platform isEqualToString:@"iPad16,5"] ||
[platform isEqualToString:@"iPad16,6"])
return @"iPad Pro 12.9 inch (7th gen)";
-
+
if ([platform isEqualToString:@"iPad16,8"] ||
[platform isEqualToString:@"iPad16,9"])
return @"iPad Air 11 inch (8th gen)";
-
+
if ([platform isEqualToString:@"iPad16,10"] ||
[platform isEqualToString:@"iPad16,11"])
return @"iPad Air 13 inch (8th gen)";
-
+
if ([platform hasPrefix:@"iPhone"])
return @"Unknown iPhone";
if ([platform hasPrefix:@"iPod"])
return @"Unknown iPod";
if ([platform hasPrefix:@"iPad"])
return @"Unknown iPad";
-
+
if ([platform hasSuffix:@"86"] || [platform isEqual:@"x86_64"] || [platform isEqual:@"arm64"]) {
return @"iPhone Simulator";
}
#else
return [self macHWName];
#endif
-
+
return @"Unknown iOS device";
}
-
+
- (NSString *)macHWName {
size_t len = 0;
sysctlbyname("hw.model", NULL, &len, NULL, 0);
@@ -779,12 +784,12 @@ NSString *suffix = @"";
{
size_t size;
sysctlbyname(typeSpecifier, NULL, &size, NULL, 0);
-
+
char *answer = malloc(size);
sysctlbyname(typeSpecifier, answer, &size, NULL, 0);
-
+
NSString *results = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding];
-
+
free(answer);
return results;
}
@@ -796,15 +801,15 @@ NSString *suffix = @"";
- (MTApiEnvironment *)withUpdatedLangPackCode:(NSString *)langPackCode {
MTApiEnvironment *result = [[MTApiEnvironment alloc] initWithDeviceModelName:_deviceModelName];
-
+
result.apiId = self.apiId;
result.appVersion = self.appVersion;
result.layer = self.layer;
-
+
result.langPack = self.langPack;
-
+
result->_langPackCode = langPackCode;
-
+
result.disableUpdates = self.disableUpdates;
result.tcpPayloadPrefix = self.tcpPayloadPrefix;
result.datacenterAddressOverrides = self.datacenterAddressOverrides;
@@ -812,105 +817,105 @@ NSString *suffix = @"";
result->_socksProxySettings = self.socksProxySettings;
result->_networkSettings = self.networkSettings;
result->_systemCode = self.systemCode;
-
+
[result _updateApiInitializationHash];
-
+
return result;
}
- (instancetype)copyWithZone:(NSZone *)__unused zone {
MTApiEnvironment *result = [[MTApiEnvironment alloc] initWithDeviceModelName:_deviceModelName];
-
+
result.apiId = self.apiId;
result.appVersion = self.appVersion;
result.layer = self.layer;
-
+
result.langPack = self.langPack;
-
+
result->_langPackCode = self.langPackCode;
result->_socksProxySettings = self.socksProxySettings;
result->_networkSettings = self.networkSettings;
result->_systemCode = self.systemCode;
-
+
result.disableUpdates = self.disableUpdates;
result.tcpPayloadPrefix = self.tcpPayloadPrefix;
result.datacenterAddressOverrides = self.datacenterAddressOverrides;
result.accessHostOverride = self.accessHostOverride;
-
+
[result _updateApiInitializationHash];
-
+
return result;
}
- (MTApiEnvironment *)withUpdatedSocksProxySettings:(MTSocksProxySettings *)socksProxySettings {
MTApiEnvironment *result = [[MTApiEnvironment alloc] initWithDeviceModelName:_deviceModelName];
-
+
result.apiId = self.apiId;
result.appVersion = self.appVersion;
result.layer = self.layer;
-
+
result.langPack = self.langPack;
-
+
result->_langPackCode = self.langPackCode;
result->_socksProxySettings = socksProxySettings;
result->_networkSettings = self.networkSettings;
result->_systemCode = self.systemCode;
-
+
result.disableUpdates = self.disableUpdates;
result.tcpPayloadPrefix = self.tcpPayloadPrefix;
result.datacenterAddressOverrides = self.datacenterAddressOverrides;
result.accessHostOverride = self.accessHostOverride;
-
+
[result _updateApiInitializationHash];
-
+
return result;
}
- (MTApiEnvironment *)withUpdatedNetworkSettings:(MTNetworkSettings *)networkSettings {
MTApiEnvironment *result = [[MTApiEnvironment alloc] initWithDeviceModelName:_deviceModelName];
-
+
result.apiId = self.apiId;
result.appVersion = self.appVersion;
result.layer = self.layer;
-
+
result.langPack = self.langPack;
-
+
result->_langPackCode = self.langPackCode;
result->_socksProxySettings = self.socksProxySettings;
result->_networkSettings = networkSettings;
result->_systemCode = self.systemCode;
-
+
result.disableUpdates = self.disableUpdates;
result.tcpPayloadPrefix = self.tcpPayloadPrefix;
result.datacenterAddressOverrides = self.datacenterAddressOverrides;
result.accessHostOverride = self.accessHostOverride;
-
+
[result _updateApiInitializationHash];
-
+
return result;
}
- (MTApiEnvironment *)withUpdatedSystemCode:(NSData *)systemCode {
MTApiEnvironment *result = [[MTApiEnvironment alloc] initWithDeviceModelName:_deviceModelName];
-
+
result.apiId = self.apiId;
result.appVersion = self.appVersion;
result.layer = self.layer;
-
+
result.langPack = self.langPack;
-
+
result->_langPackCode = self.langPackCode;
result->_socksProxySettings = self.socksProxySettings;
result->_networkSettings = self.networkSettings;
result->_systemCode = systemCode;
-
+
result.disableUpdates = self.disableUpdates;
result.tcpPayloadPrefix = self.tcpPayloadPrefix;
result.datacenterAddressOverrides = self.datacenterAddressOverrides;
result.accessHostOverride = self.accessHostOverride;
-
+
[result _updateApiInitializationHash];
-
+
return result;
}
diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
index e825015ca7..999fa056f5 100644
--- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
+++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
@@ -342,7 +342,7 @@ public enum PremiumSource: Equatable {
}
}
}
-
+
case settings
case stickers
case reactions
@@ -392,7 +392,7 @@ public enum PremiumSource: Equatable {
case aiTools
case auth(String, Int32)
case premiumGift(TelegramMediaFile)
-
+
var identifier: String? {
switch self {
case .settings:
@@ -526,7 +526,7 @@ public enum PremiumPerk: CaseIterable {
case todo
case copyProtection
case aiTools
-
+
case businessLocation
case businessHours
case businessGreetingMessage
@@ -535,7 +535,7 @@ public enum PremiumPerk: CaseIterable {
case businessChatBots
case businessIntro
case businessLinks
-
+
public static var allCases: [PremiumPerk] {
return [
.doubleLimits,
@@ -566,7 +566,7 @@ public enum PremiumPerk: CaseIterable {
.aiTools
]
}
-
+
public static var allBusinessCases: [PremiumPerk] {
return [
.businessLocation,
@@ -579,8 +579,8 @@ public enum PremiumPerk: CaseIterable {
.businessChatBots
]
}
-
-
+
+
init?(identifier: String, business: Bool) {
for perk in business ? PremiumPerk.allBusinessCases : PremiumPerk.allCases {
if perk.identifier == identifier {
@@ -590,7 +590,7 @@ public enum PremiumPerk: CaseIterable {
}
return nil
}
-
+
var identifier: String {
switch self {
case .doubleLimits:
@@ -663,7 +663,7 @@ public enum PremiumPerk: CaseIterable {
return "business_links"
}
}
-
+
func title(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
@@ -736,7 +736,7 @@ public enum PremiumPerk: CaseIterable {
return strings.Business_Links
}
}
-
+
func subtitle(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
@@ -809,7 +809,7 @@ public enum PremiumPerk: CaseIterable {
return strings.Business_LinksInfo
}
}
-
+
var iconName: String {
switch self {
case .doubleLimits:
@@ -923,15 +923,15 @@ struct PremiumIntroConfiguration {
.businessChatBots
])
}
-
+
let perks: [PremiumPerk]
let businessPerks: [PremiumPerk]
-
+
fileprivate init(perks: [PremiumPerk], businessPerks: [PremiumPerk]) {
self.perks = perks
self.businessPerks = businessPerks
}
-
+
public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration {
if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] {
var perks: [PremiumPerk] = []
@@ -951,13 +951,13 @@ struct PremiumIntroConfiguration {
if perks.count < 4 {
perks = PremiumIntroConfiguration.defaultValue.perks
}
-
+
#if DEBUG
if !perks.contains(.aiTools) {
perks.insert(.aiTools, at: 1)
}
#endif
-
+
var businessPerks: [PremiumPerk] = []
if let values = data["business_promo_order"] as? [String] {
for value in values {
@@ -974,7 +974,7 @@ struct PremiumIntroConfiguration {
if businessPerks.count < 4 {
businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks
}
-
+
return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks)
} else {
return .defaultValue
@@ -985,15 +985,15 @@ struct PremiumIntroConfiguration {
private struct PremiumProduct: Equatable {
let option: PremiumPromoConfiguration.PremiumProductOption
let storeProduct: InAppPurchaseManager.Product?
-
+
var id: String {
return self.storeProduct?.id ?? self.option.botUrl
}
-
+
var months: Int32 {
return self.option.months
}
-
+
var price: String {
if let storeProduct = self.storeProduct {
return storeProduct.price
@@ -1001,7 +1001,7 @@ private struct PremiumProduct: Equatable {
return formatCurrencyAmount(self.option.amount, currency: self.option.currency)
}
}
-
+
var pricePerMonth: String {
if let storeProduct = self.storeProduct {
return storeProduct.pricePerMonth(Int(self.months))
@@ -1009,7 +1009,7 @@ private struct PremiumProduct: Equatable {
return formatCurrencyAmount(self.option.amount / Int64(self.months), currency: self.option.currency)
}
}
-
+
var priceCurrencyAndAmount: (currency: String, amount: Int64) {
if let priceCurrencyAndAmount = self.storeProduct?.priceCurrencyAndAmount {
return priceCurrencyAndAmount
@@ -1017,7 +1017,7 @@ private struct PremiumProduct: Equatable {
return (self.option.currency, self.option.amount)
}
}
-
+
var priceValue: NSDecimalNumber {
if let priceValue = self.storeProduct?.priceValue {
return priceValue
@@ -1025,15 +1025,15 @@ private struct PremiumProduct: Equatable {
return self.optionPriceValue
}
}
-
+
var optionPriceValue: NSDecimalNumber {
return currencyToFractionalAmount(value: self.option.amount, currency: self.option.currency).flatMap { NSDecimalNumber(floatLiteral: $0) } ?? 0.0
}
-
+
var isCurrent: Bool {
return self.option.isCurrent
}
-
+
var transactionId: String? {
return self.option.transactionId
}
@@ -1042,7 +1042,7 @@ private struct PremiumProduct: Equatable {
final class PerkIconComponent: CombinedComponent {
let backgroundColor: UIColor
let iconName: String
-
+
init(
backgroundColor: UIColor,
iconName: String
@@ -1050,7 +1050,7 @@ final class PerkIconComponent: CombinedComponent {
self.backgroundColor = backgroundColor
self.iconName = iconName
}
-
+
static func ==(lhs: PerkIconComponent, rhs: PerkIconComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
@@ -1060,7 +1060,7 @@ final class PerkIconComponent: CombinedComponent {
}
return true
}
-
+
static var body: Body {
let image = Child(Image.self)
return { context in
@@ -1087,14 +1087,14 @@ final class SectionGroupComponent: Component {
public let accessibilityLabel: String
public let isEnabled: Bool
public let action: () -> Void
-
+
public init(_ content: AnyComponentWithIdentity, accessibilityLabel: String, isEnabled: Bool = true, action: @escaping () -> Void) {
self.content = content
self.accessibilityLabel = accessibilityLabel
self.isEnabled = isEnabled
self.action = action
}
-
+
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.content != rhs.content {
return false
@@ -1108,12 +1108,12 @@ final class SectionGroupComponent: Component {
return true
}
}
-
+
public let items: [Item]
public let backgroundColor: UIColor
public let selectionColor: UIColor
public let separatorColor: UIColor
-
+
public init(
items: [Item],
backgroundColor: UIColor,
@@ -1125,7 +1125,7 @@ final class SectionGroupComponent: Component {
self.selectionColor = selectionColor
self.separatorColor = separatorColor
}
-
+
public static func ==(lhs: SectionGroupComponent, rhs: SectionGroupComponent) -> Bool {
if lhs.items != rhs.items {
return false
@@ -1141,49 +1141,49 @@ final class SectionGroupComponent: Component {
}
return true
}
-
+
public final class View: UIView {
private var buttonViews: [AnyHashable: HighlightTrackingButton] = [:]
private var itemViews: [AnyHashable: ComponentHostView] = [:]
private var separatorViews: [UIView] = []
-
+
private var component: SectionGroupComponent?
-
+
override init(frame: CGRect) {
super.init(frame: frame)
}
-
+
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
@objc private func buttonPressed(_ sender: HighlightTrackingButton) {
guard let component = self.component else {
return
}
-
+
if let (id, _) = self.buttonViews.first(where: { $0.value === sender }), let item = component.items.first(where: { $0.content.id == id }) {
item.action()
}
}
-
+
func update(component: SectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
let sideInset: CGFloat = 16.0
-
+
self.backgroundColor = component.backgroundColor
-
+
var size = CGSize(width: availableSize.width, height: 0.0)
-
+
var validIds: [AnyHashable] = []
-
+
var i = 0
for item in component.items {
validIds.append(item.content.id)
-
+
let buttonView: HighlightTrackingButton
let itemView: ComponentHostView
var itemTransition = transition
-
+
if let current = self.buttonViews[item.content.id] {
buttonView = current
} else {
@@ -1195,7 +1195,7 @@ final class SectionGroupComponent: Component {
self.addSubview(buttonView)
}
buttonView.accessibilityLabel = item.accessibilityLabel
-
+
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
@@ -1212,12 +1212,12 @@ final class SectionGroupComponent: Component {
)
buttonView.isEnabled = item.isEnabled
itemView.alpha = item.isEnabled ? 1.0 : 0.3
-
+
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel))
itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset, y: itemFrame.minY + floor((itemFrame.height - itemSize.height) / 2.0)), size: itemSize)
itemView.isUserInteractionEnabled = false
-
+
buttonView.highligthedChanged = { [weak buttonView] highlighted in
if highlighted {
buttonView?.backgroundColor = component.selectionColor
@@ -1227,9 +1227,9 @@ final class SectionGroupComponent: Component {
})
}
}
-
+
size.height += itemSize.height
-
+
if i != component.items.count - 1 {
let separatorView: UIView
if self.separatorViews.count > i {
@@ -1240,12 +1240,12 @@ final class SectionGroupComponent: Component {
self.addSubview(separatorView)
}
separatorView.backgroundColor = component.separatorColor
-
+
separatorView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset * 2.0 + 30.0, y: itemFrame.maxY), size: CGSize(width: size.width - sideInset * 2.0 - 30.0, height: UIScreenPixel))
}
i += 1
}
-
+
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
@@ -1256,24 +1256,24 @@ final class SectionGroupComponent: Component {
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
-
+
if !self.separatorViews.isEmpty, self.separatorViews.count > component.items.count - 1 {
for i in (component.items.count - 1) ..< self.separatorViews.count {
self.separatorViews[i].removeFromSuperview()
}
self.separatorViews.removeSubrange((component.items.count - 1) ..< self.separatorViews.count)
}
-
+
self.component = component
-
+
return size
}
}
-
+
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, state: state, environment: environment, transition: transition)
}
@@ -1290,7 +1290,7 @@ final class PerkComponent: CombinedComponent {
let accentColor: UIColor
let displayArrow: Bool
let badge: String?
-
+
init(
iconName: String,
iconBackgroundColors: [UIColor],
@@ -1314,7 +1314,7 @@ final class PerkComponent: CombinedComponent {
self.displayArrow = displayArrow
self.badge = badge
}
-
+
static func ==(lhs: PerkComponent, rhs: PerkComponent) -> Bool {
if lhs.iconName != rhs.iconName {
return false
@@ -1348,7 +1348,7 @@ final class PerkComponent: CombinedComponent {
}
return true
}
-
+
static var body: Body {
let iconBackground = Child(RoundedRectangle.self)
let icon = Child(BundleIconComponent.self)
@@ -1360,14 +1360,14 @@ final class PerkComponent: CombinedComponent {
return { context in
let component = context.component
-
+
let sideInset: CGFloat = 16.0
let iconTopInset: CGFloat = 15.0
let textTopInset: CGFloat = 9.0
let textBottomInset: CGFloat = 9.0
let spacing: CGFloat = 2.0
let iconSize = CGSize(width: 30.0, height: 30.0)
-
+
let iconBackground = iconBackground.update(
component: RoundedRectangle(
colors: component.iconBackgroundColors,
@@ -1376,7 +1376,7 @@ final class PerkComponent: CombinedComponent {
availableSize: iconSize,
transition: context.transition
)
-
+
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
@@ -1385,7 +1385,7 @@ final class PerkComponent: CombinedComponent {
availableSize: iconSize,
transition: context.transition
)
-
+
let title = title.update(
component: MultilineTextComponent(
text: .plain(
@@ -1401,7 +1401,7 @@ final class PerkComponent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - iconBackground.size.width - sideInset * 2.83, height: context.availableSize.height),
transition: context.transition
)
-
+
let subtitle = subtitle.update(
component: MultilineTextComponent(
text: .plain(
@@ -1417,27 +1417,27 @@ final class PerkComponent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - iconBackground.size.width - sideInset * 2.83, height: context.availableSize.height),
transition: context.transition
)
-
+
let iconPosition = CGPoint(x: iconBackground.size.width / 2.0, y: iconTopInset + iconBackground.size.height / 2.0)
context.add(iconBackground
.position(iconPosition)
)
-
+
context.add(icon
.position(iconPosition)
)
-
+
context.add(title
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
-
+
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
-
+
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
@@ -1447,22 +1447,22 @@ final class PerkComponent: CombinedComponent {
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
-
+
context.add(badgeBackground
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
)
-
+
context.add(badgeText
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
)
}
-
+
context.add(subtitle
.position(CGPoint(x: iconBackground.size.width + sideInset + subtitle.size.width / 2.0, y: textTopInset + title.size.height + spacing + subtitle.size.height / 2.0))
)
-
+
let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + subtitle.size.height + textBottomInset)
-
+
if component.displayArrow {
let arrow = arrow.update(
component: BundleIconComponent(
@@ -1476,7 +1476,7 @@ final class PerkComponent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0))
)
}
-
+
return size
}
}
@@ -1485,7 +1485,7 @@ final class PerkComponent: CombinedComponent {
private final class PremiumIntroScreenContentComponent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
-
+
let screenContext: PremiumIntroScreen.ScreenContext
let mode: PremiumIntroScreen.Mode
let source: PremiumSource
@@ -1504,7 +1504,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let updateIsFocused: (Bool) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
-
+
init(
screenContext: PremiumIntroScreen.ScreenContext,
mode: PremiumIntroScreen.Mode,
@@ -1544,7 +1544,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
self.copyLink = copyLink
self.shareLink = shareLink
}
-
+
static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool {
if lhs.source != rhs.source {
return false
@@ -1573,44 +1573,44 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.promoConfiguration != rhs.promoConfiguration {
return false
}
-
+
return true
}
-
+
final class State: ComponentState {
private let screenContext: PremiumIntroScreen.ScreenContext
private let present: (ViewController) -> Void
-
+
var products: [PremiumProduct]?
var selectedProductId: String?
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
-
+
var newPerks: [String] = []
-
+
var isPremium: Bool?
var peer: EnginePeer?
var adsEnabled = false
-
+
private var disposable: Disposable?
private(set) var configuration = PremiumIntroConfiguration.defaultValue
-
+
private var stickersDisposable: Disposable?
private var newPerksDisposable: Disposable?
private var preloadDisposableSet = DisposableSet()
private var adsEnabledDisposable: Disposable?
-
+
var price: String? {
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
}
-
+
var isAnnual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
-
+
var isBiannual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.months == 24
}
-
+
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
@@ -1622,9 +1622,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
return false
}
}
-
+
var cachedChevronImage: (UIImage, PresentationTheme)?
-
+
init(
screenContext: PremiumIntroScreen.ScreenContext,
source: PremiumSource,
@@ -1632,11 +1632,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
) {
self.screenContext = screenContext
self.present = present
-
+
super.init()
-
+
self.newPerks = [PremiumPerk.aiTools.identifier, PremiumPerk.copyProtection.identifier]
-
+
let premiumIntroConfiguration: Signal
let accountPeer: Signal
switch screenContext {
@@ -1650,7 +1650,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
premiumIntroConfiguration = .single(PremiumIntroConfiguration.defaultValue)
accountPeer = .single(nil)
}
-
+
self.disposable = combineLatest(
queue: Queue.mainQueue(),
premiumIntroConfiguration,
@@ -1660,15 +1660,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
return
}
let isFirstTime = self.peer == nil
-
+
self.configuration = premiumIntroConfiguration
self.peer = accountPeer
self.updated(transition: .immediate)
-
+
if let identifier = source.identifier, isFirstTime {
var jsonString: String = "{"
jsonString += "\"source\": \"\(identifier)\","
-
+
jsonString += "\"data\": {\"premium_promo_order\":["
var isFirst = true
for perk in premiumIntroConfiguration.perks {
@@ -1679,16 +1679,16 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
jsonString += "\"\(perk.identifier)\""
}
jsonString += "]}}"
-
+
if let context = screenContext.context, let data = jsonString.data(using: .utf8), let json = JSON(data: data) {
context.engine.accountData.addAppLogEvent(type: "premium.promo_screen_show", data: json)
}
}
})
-
+
if let context = screenContext.context {
let _ = updatePremiumPromoConfigurationOnce(account: context.account).start()
-
+
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers)
self.stickersDisposable = (context.account.postbox.combinedView(keys: [stickersKey])
|> deliverOnMainQueue).start(next: { [weak self] views in
@@ -1707,7 +1707,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
}
})
-
+
self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] adsEnabled in
guard let self else {
@@ -1718,7 +1718,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
})
}
}
-
+
deinit {
self.disposable?.dispose()
self.preloadDisposableSet.dispose()
@@ -1726,9 +1726,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
self.newPerksDisposable?.dispose()
self.adsEnabledDisposable?.dispose()
}
-
+
private var updatedPeerStatus: PeerEmojiStatus?
-
+
private weak var emojiStatusSelectionController: ViewController?
private var previousEmojiSetupTimestamp: Double?
func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) {
@@ -1740,13 +1740,13 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
return
}
self.previousEmojiSetupTimestamp = currentTimestamp
-
+
self.emojiStatusSelectionController?.dismiss()
var selectedItems = Set()
if let currentFileId {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId))
}
-
+
let controller = EmojiStatusSelectionController(
context: context,
mode: .statusSelection,
@@ -1779,11 +1779,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
self.present(controller)
}
}
-
+
func makeState() -> State {
return State(screenContext: self.screenContext, source: self.source, present: self.present)
}
-
+
static var body: Body {
let overscroll = Child(Rectangle.self)
let fade = Child(RoundedRectangle.self)
@@ -1799,10 +1799,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let infoTitle = Child(MultilineTextComponent.self)
let infoText = Child(MultilineTextComponent.self)
let termsText = Child(MultilineTextComponent.self)
-
+
return { context in
let sideInset: CGFloat = 16.0
-
+
let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
@@ -1810,21 +1810,21 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
state.selectedProductId = context.component.selectedProductId
state.validPurchases = context.component.validPurchases
state.isPremium = context.component.isPremium
-
+
let theme = environment.theme
let strings = environment.strings
let presentationData = context.component.screenContext.presentationData
-
+
let availableWidth = context.availableSize.width
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
var size = CGSize(width: context.availableSize.width, height: 0.0)
-
+
var topBackgroundColor = theme.list.plainBackgroundColor
let bottomBackgroundColor = theme.list.blocksBackgroundColor
if theme.overallDarkAppearance {
topBackgroundColor = bottomBackgroundColor
}
-
+
let overscroll = overscroll.update(
component: Rectangle(color: topBackgroundColor),
availableSize: CGSize(width: context.availableSize.width, height: 1000),
@@ -1833,7 +1833,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
context.add(overscroll
.position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0))
)
-
+
let fade = fade.update(
component: RoundedRectangle(
colors: [
@@ -1849,16 +1849,16 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
context.add(fade
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
)
-
+
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
-
+
let textColor = theme.list.itemPrimaryTextColor
let accentColor = theme.list.itemAccentColor
let subtitleColor = theme.list.itemSecondaryTextColor
-
+
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
-
+
var link = ""
let textString: String
if case .premiumGift = context.component.source {
@@ -1903,11 +1903,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
textString = strings.Premium_Description
}
}
-
+
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
-
+
let shareLink = context.component.shareLink
let textComponent: _ConcreteChildComponent
if context.component.justBought {
@@ -1949,7 +1949,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
size.height += text.size.height
size.height += 21.0
-
+
let gradientColors: [UIColor] = [
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe95a2c),
@@ -1977,27 +1977,27 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
UIColor(rgb: 0x3eb26d),
UIColor(rgb: 0x3dbd4a)
]
-
+
let accountContext = context.component.screenContext.context
let present = context.component.present
let push = context.component.push
let selectProduct = context.component.selectProduct
let buy = context.component.buy
let updateIsFocused = context.component.updateIsFocused
-
+
let layoutOptions = {
if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) {
var optionsItems: [SectionGroupComponent.Item] = []
-
+
let shortestProductPrice: (Int64, NSDecimalNumber)
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestProductPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestProductPrice = (1, NSDecimalNumber(decimal: 1))
}
-
+
let currentProductMonths = state.products?.first(where: { $0.isCurrent })?.months ?? 0
-
+
var referenceProduct: InAppPurchaseManager.Product?
for product in products {
if let storeProduct = product.storeProduct {
@@ -2005,7 +2005,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
break
}
}
-
+
var i = 0
for product in products {
let giftTitle: String
@@ -2018,7 +2018,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} else {
giftTitle = strings.Premium_Annual
}
-
+
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestProductPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
let discount: String
@@ -2027,18 +2027,18 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} else {
discount = ""
}
-
+
var defaultPrice: String = ""
if let referenceProduct {
defaultPrice = referenceProduct.defaultPrice(shortestProductPrice.1, monthsCount: Int(product.months))
}
-
+
var subtitle = ""
var accessibilitySubtitle = ""
var pricePerMonth = product.price
if product.months > 1 {
pricePerMonth = product.pricePerMonth
-
+
if discountValue > 0 {
subtitle = "**\(defaultPrice)** \(product.price)"
accessibilitySubtitle = product.price
@@ -2060,7 +2060,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
accessibilitySubtitle = subtitle
}
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
-
+
optionsItems.append(
SectionGroupComponent.Item(
AnyComponentWithIdentity(
@@ -2089,7 +2089,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
i += 1
}
-
+
let optionsSection = optionsSection.update(
component: SectionGroupComponent(
items: optionsItems,
@@ -2107,7 +2107,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
.cornerRadius(26.0)
)
size.height += optionsSection.size.height
-
+
if case .emojiStatus = context.component.source {
size.height -= 18.0
} else {
@@ -2115,20 +2115,20 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
}
}
-
+
let textSideInset: CGFloat = 16.0
-
+
let forceDark = context.component.forceDark
let layoutPerks = {
size.height += 8.0
-
+
var i = 0
var perksItems: [AnyComponentWithIdentity] = []
for perk in state.configuration.perks {
if case .business = context.component.mode, case .business = perk {
continue
}
-
+
let isNew = state.newPerks.contains(perk.identifier)
let titleComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -2138,7 +2138,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)),
maximumNumberOfLines: 0
))
-
+
let titleCombinedComponent: AnyComponent
if isNew {
titleCombinedComponent = AnyComponent(HStack([
@@ -2148,7 +2148,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} else {
titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0))
}
-
+
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
@@ -2241,7 +2241,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
buttonText = strings.Premium_SubscribeFor(state?.price ?? "–").string
}
}
-
+
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: buttonText, isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
@@ -2265,7 +2265,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
))))
i += 1
}
-
+
let perksSection = perksSection.update(
component: ListSectionComponent(
theme: environment.theme,
@@ -2290,7 +2290,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
.disappear(.default(alpha: true))
)
size.height += perksSection.size.height
-
+
if case .emojiStatus = context.component.source {
if state.isPremium == true {
size.height -= 23.0
@@ -2301,10 +2301,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += 23.0
}
}
-
+
let layoutBusinessPerks = {
size.height += 8.0
-
+
let gradientColors: [UIColor] = [
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe54937),
@@ -2315,7 +2315,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
UIColor(rgb: 0x676bff),
UIColor(rgb: 0x0088ff)
]
-
+
var i = 0
var perksItems: [AnyComponentWithIdentity] = []
for perk in state.configuration.businessPerks {
@@ -2328,7 +2328,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)),
maximumNumberOfLines: 0
))
-
+
let titleCombinedComponent: AnyComponent
if isNew {
titleCombinedComponent = AnyComponent(HStack([
@@ -2338,7 +2338,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} else {
titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0))
}
-
+
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
@@ -2361,7 +2361,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
guard let accountContext else {
return
}
-
+
let isPremium = state?.isPremium == true
if isPremium {
switch perk {
@@ -2464,8 +2464,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
default:
fatalError()
}
-
-
+
+
let buttonText: String
if isPremium {
buttonText = strings.Common_OK
@@ -2478,7 +2478,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
buttonText = strings.Premium_SubscribeFor(state?.price ?? "–").string
}
}
-
+
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, buttonText: buttonText, isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
@@ -2500,7 +2500,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
))))
i += 1
}
-
+
let businessSection = businessSection.update(
component: ListSectionComponent(
theme: environment.theme,
@@ -2519,12 +2519,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += businessSection.size.height
size.height += 23.0
}
-
+
let layoutMoreBusinessPerks = {
size.height += 8.0
-
+
let status = state.peer?.emojiStatus
-
+
let accentColor = environment.theme.list.itemAccentColor
var perksItems: [AnyComponentWithIdentity] = []
if let accountContext = context.component.screenContext.context {
@@ -2569,7 +2569,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
))))
}
-
+
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
@@ -2603,7 +2603,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
}
))))
-
+
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
@@ -2637,7 +2637,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false))
}
))))
-
+
let moreBusinessSection = moreBusinessSection.update(
component: ListSectionComponent(
theme: environment.theme,
@@ -2670,7 +2670,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += moreBusinessSection.size.height
size.height += 23.0
}
-
+
let termsFont = Font.regular(13.0)
let boldTermsFont = Font.semibold(13.0)
let italicTermsFont = Font.italic(13.0)
@@ -2680,10 +2680,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
-
+
let layoutAdsSettings = {
size.height += 8.0
-
+
var adsSettingsItems: [AnyComponentWithIdentity] = []
adsSettingsItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
@@ -2708,7 +2708,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
action: nil,
tag: doNotHideAdsTag
))))
-
+
let adsInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Business_AdsInfo, attributes: termsMarkdownAttributes, textAlignment: .natural
))
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme {
@@ -2763,7 +2763,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += adsSettingsSection.size.height
size.height += 23.0
}
-
+
let copyLink = context.component.copyLink
if case .emojiStatus = context.component.source {
layoutPerks()
@@ -2790,11 +2790,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += linkButton.size.height
size.height += 17.0
}
-
+
layoutPerks()
} else {
layoutOptions()
-
+
if case .business = context.component.mode {
layoutBusinessPerks()
if context.component.isPremium == true {
@@ -2803,9 +2803,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
} else {
layoutPerks()
-
+
let textPadding: CGFloat = 17.0
-
+
let infoTitle = infoTitle.update(
component: MultilineTextComponent(
text: .plain(
@@ -2824,7 +2824,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
size.height += infoTitle.size.height
size.height += 3.0
-
+
let infoText = infoText.update(
component: MultilineTextComponent(
text: .markdown(
@@ -2839,7 +2839,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
-
+
let infoBackground = infoBackground.update(
component: RoundedRectangle(
color: environment.theme.list.itemBlocksBackgroundColor,
@@ -2857,14 +2857,14 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
size.height += infoBackground.size.height
size.height += 6.0
-
+
var isGiftView = false
if case let .gift(fromId, _, _, _) = context.component.source {
if let accountContext = context.component.screenContext.context, fromId == accountContext.account.peerId {
isGiftView = true
}
}
-
+
let termsString: MultilineTextComponent.TextContent
if isGiftView {
termsString = .plain(NSAttributedString())
@@ -2877,7 +2877,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
attributes: termsMarkdownAttributes
)
}
-
+
let controller = environment.controller
let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = controller() as? PremiumIntroScreen, let context = controller.context, let navigationController = controller.navigationController as? NavigationController {
@@ -2907,7 +2907,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
}
}
-
+
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
@@ -2937,16 +2937,16 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += 10.0
}
}
-
+
size.height += scrollEnvironment.insets.bottom
if case .business = context.component.mode, state.isPremium == false {
size.height += 123.0
}
-
+
if context.component.source != .settings {
size.height += 44.0
}
-
+
return size
}
}
@@ -2954,7 +2954,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
private final class PremiumIntroScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
-
+
let overNavigationContainer: UIView
let screenContext: PremiumIntroScreen.ScreenContext
let mode: PremiumIntroScreen.Mode
@@ -2967,7 +2967,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let completion: () -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
-
+
init(overNavigationContainer: UIView, screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) {
self.overNavigationContainer = overNavigationContainer
self.screenContext = screenContext
@@ -2982,7 +2982,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.copyLink = copyLink
self.shareLink = shareLink
}
-
+
static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool {
if lhs.mode != rhs.mode {
return false
@@ -2998,7 +2998,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
return true
}
-
+
final class State: ComponentState {
private let screenContext: PremiumIntroScreen.ScreenContext
private let source: PremiumSource
@@ -3006,45 +3006,45 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
private let present: (ViewController) -> Void
var navigationController: (() -> NavigationController?)?
private let completion: () -> Void
-
+
var topContentOffset: CGFloat?
var bottomContentOffset: CGFloat?
-
+
var hasIdleAnimations = true
-
+
var inProgress = false
-
+
private(set) var promoConfiguration: PremiumPromoConfiguration?
-
+
private(set) var products: [PremiumProduct]?
private(set) var selectedProductId: String?
fileprivate var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
-
+
var isPremium: Bool?
var otherPeerName: String?
var justBought = false
-
+
var emojiFile: TelegramMediaFile?
var emojiPackTitle: String?
private var emojiFileDisposable: Disposable?
-
+
private var disposable: Disposable?
private var paymentDisposable = MetaDisposable()
private var activationDisposable = MetaDisposable()
private var preloadDisposableSet = DisposableSet()
-
+
var price: String? {
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
}
-
+
var isAnnual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
-
+
var isBiannual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.months == 24
}
-
+
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
@@ -3056,7 +3056,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
return false
}
}
-
+
init(
screenContext: PremiumIntroScreen.ScreenContext,
source: PremiumSource,
@@ -3070,18 +3070,18 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.updateInProgress = updateInProgress
self.present = present
self.completion = completion
-
+
super.init()
-
+
self.validPurchases = screenContext.inAppPurchaseManager?.getReceiptPurchases() ?? []
-
+
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = screenContext.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts
} else {
availableProducts = .single([])
}
-
+
let otherPeerName: Signal
if let context = screenContext.context {
if case let .gift(fromPeerId, toPeerId, _, _) = source {
@@ -3106,11 +3106,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
otherPeerName = .single(nil)
}
-
+
if forceHasPremium {
self.isPremium = true
}
-
+
let isPremium: Signal
let promoConfiguration: Signal
switch screenContext {
@@ -3124,7 +3124,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
isPremium = .single(false)
promoConfiguration = .single(PremiumPromoConfiguration.defaultValue)
}
-
+
self.disposable = combineLatest(
queue: Queue.mainQueue(),
availableProducts,
@@ -3134,7 +3134,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
).start(next: { [weak self] availableProducts, promoConfiguration, isPremium, otherPeerName in
if let strongSelf = self {
strongSelf.promoConfiguration = promoConfiguration
-
+
let hadProducts = strongSelf.products != nil
var products: [PremiumProduct] = []
for option in promoConfiguration.premiumProductOptions {
@@ -3144,25 +3144,25 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
products.append(PremiumProduct(option: option, storeProduct: nil))
}
}
-
+
strongSelf.products = products
strongSelf.isPremium = forceHasPremium || isPremium
strongSelf.otherPeerName = otherPeerName
-
+
if !hadProducts {
strongSelf.selectedProductId = strongSelf.products?.first?.id
-
+
if let context = screenContext.context {
for (_, video) in promoConfiguration.videos {
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
}
}
}
-
+
strongSelf.updated(transition: .immediate)
}
})
-
+
if case let .emojiStatus(_, emojiFileId, emojiFile, maybeEmojiPack) = source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack {
if let emojiFile = emojiFile {
self.emojiFile = emojiFile
@@ -3182,7 +3182,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
}
}
-
+
deinit {
self.disposable?.dispose()
self.paymentDisposable.dispose()
@@ -3190,14 +3190,21 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.emojiFileDisposable?.dispose()
self.preloadDisposableSet.dispose()
}
-
+
func buy() {
guard !self.inProgress else {
return
}
-
+
let presentationData = self.screenContext.presentationData
-
+
+ if case .gift = self.source {
+ } else {
+ let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: "WinterGram", text: "In-app purchases are not available in WinterGram. To subscribe to Telegram Premium, use the official Telegram app, then sign in here — your subscription will work in WinterGram too.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
+ self.present(alertController)
+ return
+ }
+
if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil {
guard let context = self.screenContext.context else {
return
@@ -3205,17 +3212,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
-
+
self.paymentDisposable.set((context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug)
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let self else {
return
}
-
+
self.inProgress = false
self.updateInProgress(false)
self.updated(transition: .immediate)
-
+
if case let .waitForExpiration(date) = error {
let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Title, text: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Text(dateText).string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, action: { _ in return true }))
@@ -3224,7 +3231,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
guard let self else {
return
}
-
+
self.inProgress = false
self.justBought = true
self.updateInProgress(false)
@@ -3233,17 +3240,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}))
return
}
-
+
guard let inAppPurchaseManager = self.screenContext.inAppPurchaseManager,
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }) else {
return
}
-
+
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
-
+
var hasActiveSubsciption = false
if let context = self.screenContext.context, let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] {
-
+
} else if !self.validPurchases.isEmpty && !isUpgrade {
let now = Date()
for purchase in self.validPurchases.reversed() {
@@ -3252,25 +3259,25 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
}
}
-
+
if hasActiveSubsciption {
let errorText = presentationData.strings.Premium_Purchase_OnlyOneSubscriptionAllowed
let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
return
}
-
+
if let context = self.screenContext.context {
context.engine.accountData.addAppLogEvent(type: "premium.promo_screen_accept")
}
-
+
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
-
+
if let storeProduct = premiumProduct.storeProduct {
let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription
-
+
let canPurchasePremium: Signal
switch self.screenContext {
case let .accountContext(context):
@@ -3305,19 +3312,19 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
activation = .complete()
}
-
+
self.activationDisposable.set((activation
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let self {
self.inProgress = false
self.updateInProgress(false)
-
+
self.updated(transition: .immediate)
-
+
if let context = self.screenContext.context {
context.engine.accountData.addAppLogEvent(type: "premium.promo_screen_fail")
}
-
+
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
@@ -3331,10 +3338,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
self.inProgress = false
self.updateInProgress(false)
-
+
self.isPremium = true
self.justBought = true
-
+
self.updated(transition: .easeInOut(duration: 0.25))
self.completion()
}))
@@ -3346,7 +3353,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.inProgress = false
self.updateInProgress(false)
self.updated(transition: .immediate)
-
+
var errorText: String?
switch error {
case .generic:
@@ -3364,12 +3371,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
case .cancelled:
break
}
-
+
if let errorText = errorText {
if let context = self.screenContext.context {
context.engine.accountData.addAppLogEvent(type: "premium.promo_screen_fail")
}
-
+
let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
}
@@ -3382,7 +3389,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
})
} else if case let .accountContext(context) = self.screenContext, let navigationController = self.navigationController?() {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: premiumProduct.option.botUrl, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
-
+
Queue.mainQueue().after(3.0) {
self.inProgress = false
self.updateInProgress(false)
@@ -3390,22 +3397,22 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
}
}
-
+
func updateIsFocused(_ isFocused: Bool) {
self.hasIdleAnimations = !isFocused
self.updated(transition: .immediate)
}
-
+
func selectProduct(_ productId: String) {
self.selectedProductId = productId
self.updated(transition: .immediate)
}
}
-
+
func makeState() -> State {
return State(screenContext: self.screenContext, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion)
}
-
+
static var body: Body {
let background = Child(Rectangle.self)
let scrollContent = Child(ScrollComponent.self)
@@ -3416,18 +3423,18 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let secondaryTitle = Child(MultilineTextWithEntitiesComponent.self)
let bottomEdgeEffect = Child(EdgeEffectComponent.self)
let button = Child(ButtonComponent.self)
-
+
var updatedInstalled: Bool?
-
+
return { context in
let environment = context.environment[EnvironmentType.self].value
let state = context.state
state.navigationController = { [weak environment] in
return environment?.controller()?.navigationController as? NavigationController
}
-
+
let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition)
-
+
var starIsVisible = true
if let topContentOffset = state.topContentOffset, topContentOffset >= 123.0 {
starIsVisible = false
@@ -3437,7 +3444,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
if case .profile = context.component.source {
isIntro = false
}
-
+
let header: _UpdatedChildComponent
if case .business = context.component.mode {
header = coin.update(
@@ -3498,7 +3505,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
transition: context.transition
)
}
-
+
let titleString: String
if case .premiumGift = context.component.source {
titleString = environment.strings.Premium_PremiumGift_Title
@@ -3519,7 +3526,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
titleString = environment.strings.Premium_Title
}
-
+
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: titleString, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
@@ -3539,11 +3546,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
if case let .emojiStatus(peerId, _, file, maybeEmojiPack) = context.component.source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack {
loadedEmojiPack = maybeEmojiPack
highlightableLinks = true
-
+
if peerId.isGroupOrChannel, otherPeerName.count > 20 {
otherPeerName = otherPeerName.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines) + "\u{2026}"
}
-
+
var packReference: StickerPackReference?
if let file = file {
for attribute in file.attributes {
@@ -3591,7 +3598,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
secondaryTitleText = ""
}
-
+
let textColor = environment.theme.list.itemPrimaryTextColor
let accentColor: UIColor
if case .emojiStatus = context.component.source {
@@ -3599,13 +3606,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
accentColor = UIColor(rgb: 0x597cf5)
}
-
+
let textFont = Font.bold(18.0)
let boldTextFont = Font.bold(18.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: isAnonymous ? textColor : accentColor), linkAttribute: { _ in
return nil
})
-
+
let secondaryAttributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryTitleText, attributes: markdownAttributes))
if let emojiFile = state.emojiFile {
let range = (secondaryAttributedText.string as NSString).range(of: "#")
@@ -3641,7 +3648,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
if let loadedEmojiPack, case let .result(info, items, installed) = loadedEmojiPack {
loadedPack = .result(info: info, items: items, installed: updatedInstalled ?? installed)
}
-
+
let controller = context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in
return false
}, actionPerformed: { actions in
@@ -3665,12 +3672,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.width),
transition: context.transition
)
-
+
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight: CGFloat = state.isPremium == true && !state.canUpgrade ? bottomInset : bottomPanelPadding + 52.0 + bottomInset
-
+
let scrollContent = scrollContent.update(
component: ScrollComponent(
content: AnyComponent(PremiumIntroScreenContentComponent(
@@ -3719,28 +3726,28 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
availableSize: context.availableSize,
transition: context.transition
)
-
+
let topInset: CGFloat = environment.navigationHeight - 56.0
-
+
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
-
+
context.add(scrollContent
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
-
+
let titleOffset: CGFloat
let titleScale: CGFloat
let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let titleAlpha: CGFloat
-
+
if let topContentOffset = state.topContentOffset {
let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0
titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
titleScale = 1.0 - fraction * 0.36
-
+
if state.otherPeerName != nil {
titleAlpha = min(1.0, fraction * 1.1)
} else {
@@ -3751,34 +3758,34 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
titleOffset = 0.0
titleAlpha = state.otherPeerName != nil ? 0.0 : 1.0
}
-
+
context.addWithExternalContainer(header
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + header.size.height / 2.0 - 30.0 - titleOffset * titleScale))
.scale(titleScale),
container: context.component.overNavigationContainer
)
-
+
context.addWithExternalContainer(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(titleAlpha),
container: context.component.overNavigationContainer
)
-
+
context.addWithExternalContainer(secondaryTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(max(0.0, 1.0 - titleAlpha * 1.8)),
container: context.component.overNavigationContainer
)
-
+
var isUnusedGift = false
if case let .gift(fromId, _, _, giftCode) = context.component.source, let accountContext = context.component.screenContext.context {
if let giftCode, giftCode.usedDate == nil, fromId != accountContext.account.peerId {
isUnusedGift = true
}
}
-
+
var buttonIsHidden = true
if !state.justBought {
if isUnusedGift {
@@ -3789,7 +3796,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
buttonIsHidden = false
}
}
-
+
if !buttonIsHidden {
let buttonTitle: String
var buttonSubtitle: String?
@@ -3822,7 +3829,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
buttonTitle = environment.strings.Premium_SubscribeFor(state.price ?? "–").string
}
}
-
+
let controller = environment.controller
let buttonGradientColors = [
UIColor(rgb: 0x0077ff),
@@ -3880,7 +3887,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right - environment.safeInsets.left - environment.safeInsets.right, height: 52.0),
transition: context.transition)
-
+
let bottomEdgeEffectHeight = 13.0 + buttonInsets.bottom + button.size.height
let bottomEdgeEffect = bottomEdgeEffect.update(
component: EdgeEffectComponent(
@@ -3919,7 +3926,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
})
)
}
-
+
return context.availableSize
}
}
@@ -3929,7 +3936,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
public enum ScreenContext {
case accountContext(AccountContext)
case sharedContext(SharedAccountContext, TelegramEngineUnauthorized, InAppPurchaseManager)
-
+
var context: AccountContext? {
switch self {
case let .accountContext(context):
@@ -3938,7 +3945,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return nil
}
}
-
+
var sharedContext: SharedAccountContext {
switch self {
case let .accountContext(context):
@@ -3947,7 +3954,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return sharedContext
}
}
-
+
var inAppPurchaseManager: InAppPurchaseManager? {
switch self {
case let .accountContext(context):
@@ -3956,7 +3963,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return inAppPurchaseManager
}
}
-
+
var presentationData: PresentationData {
switch self {
case let .accountContext(context):
@@ -3965,7 +3972,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return sharedContext.currentPresentationData.with { $0 }
}
}
-
+
var updatedPresentationData: (initial: PresentationData, signal: Signal) {
switch self {
case let .accountContext(context):
@@ -3975,12 +3982,12 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
}
}
-
+
public enum Mode {
case premium
case business
}
-
+
fileprivate var context: AccountContext? {
switch self.screenContext {
case let .accountContext(context):
@@ -3992,45 +3999,45 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
private let screenContext: ScreenContext
fileprivate let mode: Mode
private let focusOnItemTag: PremiumIntroEntryTag?
-
+
private var didSetReady = false
private let _ready = Promise()
public override var ready: Promise {
return self._ready
}
-
+
public weak var sourceView: UIView?
public var sourceRect: CGRect?
public weak var containerView: UIView?
public var animationColor: UIColor?
-
+
private let overNavigationContainer: UIView
-
+
public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) {
self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium, focusOnItemTag: focusOnItemTag)
}
-
+
public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) {
self.screenContext = screenContext
self.mode = mode
self.focusOnItemTag = focusOnItemTag
-
+
let presentationData = screenContext.presentationData
-
+
var updateInProgressImpl: ((Bool) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var completionImpl: (() -> Void)?
var copyLinkImpl: ((String) -> Void)?
var shareLinkImpl: ((String) -> Void)?
-
+
self.overNavigationContainer = SparseContainerView()
-
+
var baseNavigationColors: BaseNavigationColors = .plain
if case .emojiStatus = source {
baseNavigationColors = .blocks
}
-
+
super.init(component: PremiumIntroScreenComponent(
overNavigationContainer: self.overNavigationContainer,
screenContext: screenContext,
@@ -4057,15 +4064,15 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
shareLinkImpl?(link)
}
), navigationBarAppearance: .default, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default, updatedPresentationData: screenContext.updatedPresentationData, baseNavigationColors: baseNavigationColors)
-
+
self._hasGlassStyle = true
-
+
if modal {
self.navigationPresentation = .modal
} else {
self.navigationPresentation = .modalInLargeLayout
}
-
+
updateInProgressImpl = { [weak self] inProgress in
guard let self else {
return
@@ -4074,7 +4081,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.view.disablesInteractiveTransitionGestureRecognizer = inProgress
self.view.disablesInteractiveModalDismiss = inProgress
}
-
+
presentImpl = { [weak self] c in
if c is UndoOverlayController {
self?.present(c, in: .current)
@@ -4082,44 +4089,44 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self?.present(c, in: .window(.root))
}
}
-
+
pushImpl = { [weak self] c in
self?.push(c)
}
-
+
completionImpl = { [weak self] in
if let self {
self.animateSuccess()
}
}
-
+
copyLinkImpl = { [weak self] link in
UIPasteboard.general.string = link
-
+
guard let self else {
return
}
self.dismissAllTooltips()
-
+
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current)
}
-
+
shareLinkImpl = { [weak self] link in
guard let self, case let .accountContext(context) = screenContext, let navigationController = self.navigationController as? NavigationController else {
return
}
-
+
let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
-
+
let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true))
peerSelectionController.peerSelected = { [weak peerSelectionController, weak navigationController] peer, threadId in
if let _ = peerSelectionController {
Queue.mainQueue().after(0.88) {
HapticFeedback().success()
}
-
+
(navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root))
-
+
let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages)
|> deliverOnMainQueue).startStandalone()
if let peerSelectionController = peerSelectionController {
@@ -4129,21 +4136,21 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
navigationController.pushViewController(peerSelectionController)
}
-
+
if case .business = mode, case let .accountContext(context) = screenContext {
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
context.account.viewTracker.keepBusinessLinksApproximatelyUpdated()
}
-
+
if let navigationBar = self.navigationBar {
navigationBar.customOverBackgroundContentView.addSubview(self.overNavigationContainer)
}
}
-
+
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
override public func viewDidLoad() {
super.viewDidLoad()
@@ -4161,12 +4168,12 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
})
}
}
-
+
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
-
+
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
@@ -4180,19 +4187,19 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return true
})
}
-
+
@objc private func cancelPressed() {
self.dismiss()
self.wasDismissed?()
}
-
+
public func animateSuccess() {
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
-
+
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
-
+
if !self.didSetReady {
if let view = findTaggedComponentViewImpl(view: self.view, tag: PremiumCoinComponent.View.Tag()) as? PremiumCoinComponent.View {
self.didSetReady = true
@@ -4200,12 +4207,12 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} else if let view = findTaggedComponentViewImpl(view: self.view, tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
-
+
if let sourceView = self.sourceView {
view.animateFrom = sourceView
view.containerView = self.containerView
view.animationColor = self.animationColor
-
+
self.sourceView = nil
self.containerView = nil
self.animationColor = nil
@@ -4213,14 +4220,14 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} else if let view = findTaggedComponentViewImpl(view: self.view, tag: EmojiHeaderComponent.View.Tag()) as? EmojiHeaderComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
-
+
if let sourceView = self.sourceView {
view.animateFrom = sourceView
view.sourceRect = self.sourceRect
view.containerView = self.containerView
-
+
view.animateIn()
-
+
self.sourceView = nil
self.containerView = nil
self.animationColor = nil
@@ -4233,7 +4240,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
private final class BadgeComponent: CombinedComponent {
let color: UIColor
let text: String
-
+
init(
color: UIColor,
text: String
@@ -4241,7 +4248,7 @@ private final class BadgeComponent: CombinedComponent {
self.color = color
self.text = text
}
-
+
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.color != rhs.color {
return false
@@ -4251,20 +4258,20 @@ private final class BadgeComponent: CombinedComponent {
}
return true
}
-
+
static var body: Body {
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
-
+
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: component.text, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
-
+
let badgeSize = CGSize(width: badgeText.size.width + 7.0, height: 16.0)
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
@@ -4274,15 +4281,15 @@ private final class BadgeComponent: CombinedComponent {
availableSize: badgeSize,
transition: context.transition
)
-
+
context.add(badgeBackground
.position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0))
)
-
+
context.add(badgeText
.position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0))
)
-
+
return badgeSize
}
}
diff --git a/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m b/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m
index 2b2977e44d..9a8550ad84 100644
--- a/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m
+++ b/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m
@@ -76,7 +76,7 @@ typedef enum {
- (void)layoutSubviews {
[super layoutSubviews];
-
+
if (_onLayout) {
_onLayout();
}
@@ -88,25 +88,25 @@ typedef enum {
{
id _didEnterBackgroundObserver;
id _willEnterBackgroundObserver;
-
+
UIColor *_backgroundColor;
UIColor *_primaryColor;
UIColor *_buttonColor;
UIColor *_accentColor;
UIColor *_regularDotColor;
UIColor *_highlightedDotColor;
-
+
TGModernButton *_alternativeLanguageButton;
-
+
SMetaDisposable *_localizationsDisposable;
TGSuggestedLocalization *_alternativeLocalizationInfo;
-
+
SVariable *_alternativeLocalization;
NSDictionary *_englishStrings;
-
+
UIView *_wrapperView;
UIView *_startButton;
-
+
bool _loadedView;
}
@end
@@ -120,14 +120,14 @@ typedef enum {
if (self != nil)
{
_isEnabled = true;
-
+
_backgroundColor = backgroundColor;
_primaryColor = primaryColor;
_buttonColor = buttonColor;
_accentColor = accentColor;
_regularDotColor = regularDotColor;
_highlightedDotColor = highlightedDotColor;
-
+
NSArray *stringKeys = @[
@"Tour.Title1",
@"Tour.Title2",
@@ -143,7 +143,7 @@ typedef enum {
@"Tour.Text6",
@"Tour.StartButton"
];
-
+
NSMutableDictionary *englishStrings = [[NSMutableDictionary alloc] init];
NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]];
for (NSString *key in stringKeys) {
@@ -159,44 +159,48 @@ typedef enum {
}
}
_englishStrings = englishStrings;
-
- _headlines = @[ _englishStrings[@"Tour.Title1"], _englishStrings[@"Tour.Title2"], _englishStrings[@"Tour.Title6"], _englishStrings[@"Tour.Title3"], _englishStrings[@"Tour.Title4"], _englishStrings[@"Tour.Title5"]];
+
+ NSString *brandTitle = _englishStrings[@"Tour.Title1"];
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"wnt_useDefaultBranding"]) {
+ brandTitle = @"Telegram";
+ }
+ _headlines = @[ brandTitle, _englishStrings[@"Tour.Title2"], _englishStrings[@"Tour.Title6"], _englishStrings[@"Tour.Title3"], _englishStrings[@"Tour.Title4"], _englishStrings[@"Tour.Title5"]];
_descriptions = @[_englishStrings[@"Tour.Text1"], _englishStrings[@"Tour.Text2"], _englishStrings[@"Tour.Text6"], _englishStrings[@"Tour.Text3"], _englishStrings[@"Tour.Text4"], _englishStrings[@"Tour.Text5"]];
-
+
__weak RMIntroViewController *weakSelf = self;
_didEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil usingBlock:^(__unused NSNotification *notification)
{
__strong RMIntroViewController *strongSelf = weakSelf;
[strongSelf stopTimer];
}];
-
+
_willEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:nil usingBlock:^(__unused NSNotification *notification)
{
__strong RMIntroViewController *strongSelf = weakSelf;
[strongSelf loadGL];
[strongSelf startTimer];
}];
-
+
_alternativeLanguageButton = [[TGModernButton alloc] init];
_alternativeLanguageButton.modernHighlight = true;
[_alternativeLanguageButton setTitleColor:accentColor];
-
+
_alternativeLanguageButton.titleLabel.font = [UIFont systemFontOfSize:18.0];
_alternativeLanguageButton.hidden = true;
[_alternativeLanguageButton addTarget:self action:@selector(alternativeLanguageButtonPressed) forControlEvents:UIControlEventTouchUpInside];
-
+
_alternativeLocalization = [[SVariable alloc] init];
-
+
_localizationsDisposable = [[suggestedLocalizationSignal deliverOn:[SQueue mainQueue]] startStrictWithNext:^(TGSuggestedLocalization *next) {
__strong RMIntroViewController *strongSelf = weakSelf;
if (strongSelf != nil && next != nil) {
if (strongSelf->_alternativeLocalizationInfo == nil) {
strongSelf->_alternativeLocalizationInfo = next;
-
+
[strongSelf->_alternativeLanguageButton setTitle:next.continueWithLanguageString forState:UIControlStateNormal];
strongSelf->_alternativeLanguageButton.hidden = false;
[strongSelf->_alternativeLanguageButton sizeToFit];
-
+
if ([strongSelf isViewLoaded]) {
strongSelf->_alternativeLanguageButton.alpha = 0.0;
[UIView animateWithDuration:0.3 animations:^{
@@ -232,22 +236,22 @@ typedef enum {
- (void)animateIn {
CGPoint logoTargetPosition = _glkView.center;
_glkView.center = CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0);
-
+
RMIntroPageView *firstPage = (RMIntroPageView *)[_pageViews firstObject];
CGPoint headerTargetPosition = firstPage.headerLabel.center;
firstPage.headerLabel.center = CGPointMake(headerTargetPosition.x, headerTargetPosition.y + 140.0);
-
+
CGPoint descriptionTargetPosition = firstPage.descriptionLabel.center;
firstPage.descriptionLabel.center = CGPointMake(descriptionTargetPosition.x, descriptionTargetPosition.y + 160.0);
-
+
CGPoint pageControlTargetPosition = _pageControl.center;
_pageControl.center = CGPointMake(pageControlTargetPosition.x, pageControlTargetPosition.y + 200.0);
-
+
CGPoint buttonTargetPosition = _startButton.center;
_startButton.center = CGPointMake(buttonTargetPosition.x, buttonTargetPosition.y + 220.0);
-
+
_glkView.transform = CGAffineTransformMakeScale(0.66, 0.66);
-
+
[UIView animateWithDuration:0.65 delay:0.15 usingSpringWithDamping:1.2f initialSpringVelocity:0.0 options:kNilOptions animations:^{
_glkView.center = logoTargetPosition;
firstPage.headerLabel.center = headerTargetPosition;
@@ -256,12 +260,12 @@ typedef enum {
_startButton.center = buttonTargetPosition;
_glkView.transform = CGAffineTransformIdentity;
} completion:nil];
-
+
_glkView.alpha = 0.0;
_pageScrollView.alpha = 0.0;
_pageControl.alpha = 0.0;
_startButton.alpha = 0.0;
-
+
[UIView animateWithDuration:0.3 delay:0.15 options:kNilOptions animations:^{
_glkView.alpha = 1.0;
_pageScrollView.alpha = 1.0;
@@ -275,23 +279,23 @@ typedef enum {
#if TARGET_OS_SIMULATOR && defined(__aarch64__)
return;
#endif
-
+
if (/*[[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground*/true && !_isOpenGLLoaded)
{
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!_context)
NSLog(@"Failed to create ES context");
-
+
bool isIpad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
-
+
CGFloat size = 200;
if (isIpad)
size *= 1.2;
-
+
int height = 50;
if (isIpad)
height += 138 / 2;
-
+
_glkView = [[GLKView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width / 2 - size / 2, height, size, size) context:_context];
_glkView.backgroundColor = _backgroundColor;
_glkView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
@@ -300,10 +304,10 @@ typedef enum {
_glkView.enableSetNeedsDisplay = false;
_glkView.userInteractionEnabled = false;
_glkView.delegate = self;
-
+
[self setupGL];
[self.view addSubview:_glkView];
-
+
[self startTimer];
_isOpenGLLoaded = true;
}
@@ -315,7 +319,7 @@ typedef enum {
return;
[self stopTimer];
-
+
if ([EAGLContext currentContext] == _glkView.context)
[EAGLContext setCurrentContext:nil];
@@ -334,26 +338,26 @@ typedef enum {
[strongSelf updateLayout];
}
};
-
+
[self viewDidLoad];
}
- (void)viewDidLoad
{
[super viewDidLoad];
-
+
if (_loadedView) {
return;
}
_loadedView = true;
-
+
self.view.backgroundColor = _backgroundColor;
-
+
[self loadGL];
-
+
_wrapperView = [[UIScrollView alloc]initWithFrame:self.view.bounds];
[self.view addSubview:_wrapperView];
-
+
_pageScrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds];
_pageScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_pageScrollView.clipsToBounds = true;
@@ -365,9 +369,9 @@ typedef enum {
_pageScrollView.contentSize = CGSizeMake(_headlines.count * self.view.bounds.size.width, self.view.bounds.size.height);
_pageScrollView.delegate = self;
[_wrapperView addSubview:_pageScrollView];
-
+
_pageViews = [NSMutableArray array];
-
+
for (NSUInteger i = 0; i < _headlines.count; i++)
{
RMIntroPageView *p = [[RMIntroPageView alloc]initWithFrame:CGRectMake(i * self.view.bounds.size.width, 0, self.view.bounds.size.width, 0) headline:[_headlines objectAtIndex:i] description:[_descriptions objectAtIndex:i] color:_primaryColor];
@@ -377,9 +381,9 @@ typedef enum {
[_pageScrollView addSubview:p];
}
[_pageScrollView setPage:0];
-
+
[self.view addSubview:_alternativeLanguageButton];
-
+
_pageControl = [[UIPageControl alloc] init];
_pageControl.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin;
_pageControl.userInteractionEnabled = false;
@@ -406,7 +410,7 @@ typedef enum {
{
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
return true;
-
+
return false;
}
@@ -414,7 +418,7 @@ typedef enum {
{
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
return UIInterfaceOrientationMaskAll;
-
+
return UIInterfaceOrientationMaskPortrait;
}
@@ -422,9 +426,9 @@ typedef enum {
{
CGSize viewSize = self.view.frame.size;
int max = (int)MAX(viewSize.width, viewSize.height);
-
+
DeviceScreen deviceScreen = Inch55;
-
+
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
{
switch (max)
@@ -432,7 +436,7 @@ typedef enum {
case 1366:
deviceScreen = iPadPro;
break;
-
+
default:
deviceScreen = iPad;
break;
@@ -459,24 +463,24 @@ typedef enum {
break;
}
}
-
+
return deviceScreen;
}
- (void)updateLayout
{
UIInterfaceOrientation isVertical = (self.view.bounds.size.height / self.view.bounds.size.width > 1.0f);
-
+
CGFloat statusBarHeight = 0;
-
+
CGFloat pageControlY = 0;
CGFloat glViewY = 0;
CGFloat startButtonY = 0;
CGFloat pageY = 0;
-
+
CGFloat languageButtonSpread = 60.0f;
CGFloat languageButtonOffset = 26.0f;
-
+
DeviceScreen deviceScreen = [self deviceScreen];
switch (deviceScreen)
{
@@ -486,14 +490,14 @@ typedef enum {
pageY = isVertical ? 485 : 335;
pageControlY = pageY + 200.0f;
break;
-
+
case iPadPro:
glViewY = isVertical ? 221 + 110 : 221;
startButtonY = 120;
pageY = isVertical ? 605 : 435;
pageControlY = pageY + 200.0f;
break;
-
+
case Inch35:
pageControlY = 162 / 2;
glViewY = 62 - 20;
@@ -509,7 +513,7 @@ typedef enum {
languageButtonSpread = 65.0f;
languageButtonOffset = 15.0f;
break;
-
+
case Inch4:
glViewY = 62;
startButtonY = 75;
@@ -518,7 +522,7 @@ typedef enum {
languageButtonSpread = 50.0f;
languageButtonOffset = 20.0f;
break;
-
+
case Inch47:
pageControlY = 162 / 2 + 10;
glViewY = 62 + 25;
@@ -526,32 +530,32 @@ typedef enum {
pageY = 245 + 50;
pageControlY = pageY + 160.0f;
break;
-
+
case Inch55:
glViewY = 62 + 45;
startButtonY = 75 + 20;
pageY = 245 + 85;
pageControlY = pageY + 160.0f;
break;
-
+
case Inch65:
glViewY = 62 + 85;
startButtonY = 75 + 30;
pageY = 245 + 125;
pageControlY = pageY + 160.0f;
break;
-
+
default:
break;
}
-
+
if (!_alternativeLanguageButton.isHidden) {
startButtonY += languageButtonSpread;
}
-
+
_pageControl.frame = CGRectMake(0, pageControlY, self.view.bounds.size.width, 7);
_glkView.frame = CGRectChangedOriginY(_glkView.frame, glViewY - statusBarHeight);
-
+
CGFloat startButtonWidth = MIN(430.0 - 48.0, self.view.bounds.size.width - 48.0f);
UIView *startButton = self.createStartButton(startButtonWidth);
if (startButton.superview == nil) {
@@ -559,14 +563,14 @@ typedef enum {
[self.view addSubview:startButton];
}
startButton.frame = CGRectMake(floor((self.view.bounds.size.width - startButtonWidth) / 2.0f), self.view.bounds.size.height - startButtonY - statusBarHeight, startButtonWidth, 50.0f);
-
+
_alternativeLanguageButton.frame = CGRectMake(floor((self.view.bounds.size.width - _alternativeLanguageButton.frame.size.width) / 2.0f), CGRectGetMaxY(startButton.frame) + languageButtonOffset, _alternativeLanguageButton.frame.size.width, _alternativeLanguageButton.frame.size.height);
-
+
_wrapperView.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height);
_pageScrollView.frame=CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20);
_pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150);
_pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0);
-
+
[_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) {
pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150);
}];
@@ -575,14 +579,14 @@ typedef enum {
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
-
+
[self loadGL];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
-
+
[self freeGL];
}
@@ -602,18 +606,18 @@ typedef enum {
{
[[NSNotificationCenter defaultCenter] removeObserver:_didEnterBackgroundObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_willEnterBackgroundObserver];
-
+
[_localizationsDisposable dispose];
-
+
[self freeGL];
}
- (void)setupGL
{
[EAGLContext setCurrentContext:_glkView.context];
-
+
UIColor *color = _backgroundColor;
-
+
CGFloat red = 0.0f;
CGFloat green = 0.0f;
CGFloat blue = 0.0f;
@@ -623,19 +627,19 @@ typedef enum {
blue = red;
}
set_intro_background_color(red, green, blue);
-
+
set_telegram_textures(setup_texture(@"telegram_sphere.png", color), setup_texture(@"telegram_plane1.png", color));
-
+
set_ic_textures(setup_texture(@"ic_bubble_dot.png", color), setup_texture(@"ic_bubble.png", color), setup_texture(@"ic_cam_lens.png", color), setup_texture(@"ic_cam.png", color), setup_texture(@"ic_pencil.png", color), setup_texture(@"ic_pin.png", color), setup_texture(@"ic_smile_eye.png", color), setup_texture(@"ic_smile.png", color), setup_texture(@"ic_videocam.png", color));
-
+
set_fast_textures(setup_texture(@"fast_body.png", color), setup_texture(@"fast_spiral.png", color), setup_texture(@"fast_arrow.png", color), setup_texture(@"fast_arrow_shadow.png", color));
-
+
set_free_textures(setup_texture(@"knot_up1.png", color), setup_texture(@"knot_down.png", color));
-
+
set_powerful_textures(setup_texture(@"powerful_mask.png", color), setup_texture(@"powerful_star.png", color), setup_texture(@"powerful_infinity.png", color), setup_texture(@"powerful_infinity_white.png", color));
-
+
set_private_textures(setup_texture(@"private_door.png", color), setup_texture(@"private_screw.png", color));
-
+
on_surface_created();
on_surface_changed(200, 200, 1, 0,0,0,0,0);
}
@@ -645,10 +649,10 @@ typedef enum {
- (void)glkView:(GLKView *)__unused view drawInRect:(CGRect)__unused rect
{
double time = CFAbsoluteTimeGetCurrent();
-
+
set_page((int)_currentPage);
set_date(time);
-
+
on_draw_frame();
}
@@ -666,28 +670,28 @@ NSInteger _current_page_end;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat offset = (scrollView.contentOffset.x - _currentPage * scrollView.frame.size.width) / self.view.frame.size.width;
-
+
set_scroll_offset((float)offset);
-
+
if (justEndDragging)
{
justEndDragging = false;
-
+
CGFloat page = scrollView.contentOffset.x / scrollView.frame.size.width;
CGFloat sign = scrollView.contentOffset.x - x;
-
+
if (sign > 0)
{
if (page > _currentPage)
_currentPage++;
}
-
+
if (sign < 0)
{
if (page < _currentPage)
_currentPage--;
}
-
+
_currentPage = MAX(0, MIN(5, _currentPage));
_current_page_end = _currentPage;
}
@@ -709,7 +713,7 @@ NSInteger _current_page_end;
}
}
}
-
+
[_pageControl setCurrentPage:_currentPage];
}
diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD
index b8a0540a12..ade087b2b1 100644
--- a/submodules/SettingsUI/BUILD
+++ b/submodules/SettingsUI/BUILD
@@ -20,6 +20,7 @@ swift_library(
"//submodules/AccountContext:AccountContext",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AlertUI:AlertUI",
+ "//submodules/PromptUI:PromptUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AvatarNode:AvatarNode",
"//submodules/CallListUI:CallListUI",
diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift
index 699e3c24de..e2a537d128 100644
--- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift
+++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift
@@ -23,6 +23,7 @@ public enum AutomaticSaveIncomingPeerType {
private final class DataAndStorageControllerArguments {
let openStorageUsage: () -> Void
let openNetworkUsage: () -> Void
+ let clearDeletedMessages: () -> Void
let openProxy: () -> Void
let openAutomaticDownloadConnectionType: (AutomaticDownloadConnectionType) -> Void
let resetAutomaticDownload: () -> Void
@@ -38,6 +39,7 @@ private final class DataAndStorageControllerArguments {
init(
openStorageUsage: @escaping () -> Void,
openNetworkUsage: @escaping () -> Void,
+ clearDeletedMessages: @escaping () -> Void,
openProxy: @escaping () -> Void,
openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void,
resetAutomaticDownload: @escaping () -> Void,
@@ -52,6 +54,7 @@ private final class DataAndStorageControllerArguments {
) {
self.openStorageUsage = openStorageUsage
self.openNetworkUsage = openNetworkUsage
+ self.clearDeletedMessages = clearDeletedMessages
self.openProxy = openProxy
self.openAutomaticDownloadConnectionType = openAutomaticDownloadConnectionType
self.resetAutomaticDownload = resetAutomaticDownload
@@ -86,7 +89,7 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable {
case autoSave(AutomaticSaveIncomingPeerType)
case sensitiveContent
case useLessVoiceData
-
+
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataAndStorageEntryTag, self == other {
return true
@@ -99,18 +102,19 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable {
private enum DataAndStorageEntry: ItemListNodeEntry {
case storageUsage(PresentationTheme, String, String)
case networkUsage(PresentationTheme, String, String)
+ case deletedMessages(PresentationTheme, String, String, Bool)
case automaticDownloadHeader(PresentationTheme, String)
case automaticDownloadCellular(PresentationTheme, String, String)
case automaticDownloadWifi(PresentationTheme, String, String)
case automaticDownloadReset(PresentationTheme, String, Bool)
-
+
case autoSaveHeader(String)
case autoSaveItem(index: Int, type: AutomaticSaveIncomingPeerType, title: String, label: String, value: String)
case autoSaveInfo(String)
-
+
case downloadInBackground(PresentationTheme, String, Bool)
case downloadInBackgroundInfo(PresentationTheme, String)
-
+
case useLessVoiceData(PresentationTheme, String, Bool)
case useLessVoiceDataInfo(PresentationTheme, String)
case otherHeader(PresentationTheme, String)
@@ -119,16 +123,16 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case pauseMusicOnRecording(PresentationTheme, String, Bool)
case raiseToListen(PresentationTheme, String, Bool)
case raiseToListenInfo(PresentationTheme, String)
-
+
case sensitiveContent(String, Bool)
case sensitiveContentInfo(String)
-
+
case connectionHeader(PresentationTheme, String)
case connectionProxy(PresentationTheme, String, String)
-
+
var section: ItemListSectionId {
switch self {
- case .storageUsage, .networkUsage:
+ case .storageUsage, .networkUsage, .deletedMessages:
return DataAndStorageSection.usage.rawValue
case .automaticDownloadHeader, .automaticDownloadCellular, .automaticDownloadWifi, .automaticDownloadReset:
return DataAndStorageSection.autoDownload.rawValue
@@ -146,25 +150,27 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return DataAndStorageSection.connection.rawValue
}
}
-
+
var stableId: Int32 {
switch self {
case .storageUsage:
return 0
case .networkUsage:
return 1
- case .automaticDownloadHeader:
+ case .deletedMessages:
return 2
- case .automaticDownloadCellular:
+ case .automaticDownloadHeader:
return 3
- case .automaticDownloadWifi:
+ case .automaticDownloadCellular:
return 4
- case .automaticDownloadReset:
+ case .automaticDownloadWifi:
return 5
- case .autoSaveHeader:
+ case .automaticDownloadReset:
return 6
+ case .autoSaveHeader:
+ return 7
case let .autoSaveItem(index, _, _, _, _):
- return 7 + Int32(index)
+ return 8 + Int32(index)
case .autoSaveInfo:
return 20
case .downloadInBackground:
@@ -197,7 +203,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return 39
}
}
-
+
static func ==(lhs: DataAndStorageEntry, rhs: DataAndStorageEntry) -> Bool {
switch lhs {
case let .storageUsage(lhsTheme, lhsText, lhsValue):
@@ -212,6 +218,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .deletedMessages(lhsTheme, lhsText, lhsValue, lhsEnabled):
+ if case let .deletedMessages(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
+ return true
+ } else {
+ return false
+ }
case let .automaticDownloadHeader(lhsTheme, lhsText):
if case let .automaticDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@@ -340,11 +352,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
}
}
}
-
+
static func <(lhs: DataAndStorageEntry, rhs: DataAndStorageEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
-
+
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DataAndStorageControllerArguments
switch self {
@@ -356,6 +368,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.dataUsage, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openNetworkUsage()
})
+ case let .deletedMessages(_, text, value, _):
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, label: value, sectionId: self.section, style: .blocks, action: {
+ arguments.clearDeletedMessages()
+ })
case let .automaticDownloadHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .automaticDownloadCellular(_, text, value):
@@ -454,7 +470,7 @@ private struct DataAndStorageData: Equatable {
let mediaInputSettings: MediaInputSettings
let voiceCallSettings: VoiceCallSettings
let proxySettings: ProxySettings?
-
+
init(automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, generatedMediaStoreSettings: GeneratedMediaStoreSettings, mediaInputSettings: MediaInputSettings, voiceCallSettings: VoiceCallSettings, proxySettings: ProxySettings?) {
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
self.autodownloadSettings = autodownloadSettings
@@ -463,7 +479,7 @@ private struct DataAndStorageData: Equatable {
self.voiceCallSettings = voiceCallSettings
self.proxySettings = proxySettings
}
-
+
static func ==(lhs: DataAndStorageData, rhs: DataAndStorageData) -> Bool {
return lhs.automaticMediaDownloadSettings == rhs.automaticMediaDownloadSettings && lhs.generatedMediaStoreSettings == rhs.generatedMediaStoreSettings && lhs.mediaInputSettings == rhs.mediaInputSettings && lhs.voiceCallSettings == rhs.voiceCallSettings && lhs.proxySettings == rhs.proxySettings
}
@@ -501,7 +517,7 @@ private func stringForAutoDownloadTypes(strings: PresentationStrings, decimalSep
if types.isEmpty {
return strings.ChatSettings_AutoDownloadSettings_OffForAll
}
-
+
var string: String = ""
for i in 0 ..< types.count {
if !string.isEmpty {
@@ -524,11 +540,11 @@ private func stringForAutoDownloadSetting(strings: PresentationStrings, decimalS
return strings.ChatSettings_AutoDownloadSettings_OffForAll
} else {
let categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
-
+
let photo = isAutodownloadEnabledForAnyPeerType(category: categories.photo)
let video = isAutodownloadEnabledForAnyPeerType(category: categories.video)
let file = isAutodownloadEnabledForAnyPeerType(category: categories.file)
-
+
return stringForAutoDownloadTypes(strings: strings, decimalSeparator: decimalSeparator, photo: photo, videoSize: video ? categories.video.sizeLimit : nil, fileSize: file ? categories.file.sizeLimit : nil)
}
}
@@ -544,7 +560,7 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings:
case .channels:
configuration = settings.configurations[.channels] ?? .default
}
-
+
for exception in settings.exceptions {
if let maybePeer = exceptionPeers[exception.id], let peer = maybePeer {
let peerTypeValue: AutomaticSaveIncomingPeerType
@@ -560,20 +576,20 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings:
peerTypeValue = .groups
}
}
-
+
if peerTypeValue == peerType {
exceptionCount += 1
}
}
}
-
+
let value: String
if configuration.photo || configuration.video {
value = presentationData.strings.Settings_AutosaveMediaOn
} else {
value = presentationData.strings.Settings_AutosaveMediaOff
}
-
+
var label = ""
if configuration.photo && configuration.video {
label.append(presentationData.strings.Settings_AutosaveMediaAllMedia(dataSizeString(Int(configuration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)
@@ -590,46 +606,48 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings:
label.append(presentationData.strings.Settings_AutosaveMediaVideo(dataSizeString(Int(configuration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)
}
}
-
+
if exceptionCount != 0 {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Notifications_CategoryExceptions(Int32(exceptionCount)))
}
-
+
return (label, value)
}
-private func dataAndStorageControllerEntries(context: AccountContext, state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings, showSensitiveContentSetting: Bool) -> [DataAndStorageEntry] {
+private func dataAndStorageControllerEntries(context: AccountContext, state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, deletedMessagesSize: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings, showSensitiveContentSetting: Bool) -> [DataAndStorageEntry] {
var entries: [DataAndStorageEntry] = []
-
+
entries.append(.storageUsage(presentationData.theme, presentationData.strings.ChatSettings_Cache, dataSizeString(storageUsage, formatting: DataSizeStringFormatting(presentationData: presentationData))))
entries.append(.networkUsage(presentationData.theme, presentationData.strings.NetworkUsageSettings_Title, dataSizeString(networkUsage, formatting: DataSizeStringFormatting(presentationData: presentationData))))
-
+ // Deleted-messages row sits below Storage/Network Usage (stableId 2).
+ entries.append(.deletedMessages(presentationData.theme, "Deleted Messages", dataSizeString(deletedMessagesSize, formatting: DataSizeStringFormatting(presentationData: presentationData)), deletedMessagesSize > 0))
+
entries.append(.automaticDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadTitle.uppercased()))
entries.append(.automaticDownloadCellular(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadUsingCellular, stringForAutoDownloadSetting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, settings: data.automaticMediaDownloadSettings, connectionType: .cellular)))
entries.append(.automaticDownloadWifi(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadUsingWiFi, stringForAutoDownloadSetting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, settings: data.automaticMediaDownloadSettings, connectionType: .wifi)))
-
+
let defaultSettings = MediaAutoDownloadSettings.defaultSettings
entries.append(.automaticDownloadReset(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadReset, data.automaticMediaDownloadSettings.cellular != defaultSettings.cellular || data.automaticMediaDownloadSettings.wifi != defaultSettings.wifi))
-
+
entries.append(.autoSaveHeader(presentationData.strings.Settings_SaveToCameraRollSection))
-
+
let privateLabelAndValue = autosaveLabelAndValue(presentationData: presentationData, settings: mediaAutoSaveSettings, peerType: .privateChats, exceptionPeers: autosaveExceptionPeers)
let groupsLabelAndValue = autosaveLabelAndValue(presentationData: presentationData, settings: mediaAutoSaveSettings, peerType: .groups, exceptionPeers: autosaveExceptionPeers)
let channelsLabelAndValue = autosaveLabelAndValue(presentationData: presentationData, settings: mediaAutoSaveSettings, peerType: .channels, exceptionPeers: autosaveExceptionPeers)
-
+
entries.append(.autoSaveItem(index: 0, type: .privateChats, title: presentationData.strings.Notifications_PrivateChats, label: privateLabelAndValue.label, value: privateLabelAndValue.value))
entries.append(.autoSaveItem(index: 1, type: .groups, title: presentationData.strings.Notifications_GroupChats, label: groupsLabelAndValue.label, value: groupsLabelAndValue.value))
entries.append(.autoSaveItem(index: 2, type: .channels, title: presentationData.strings.Notifications_Channels, label: channelsLabelAndValue.label, value: channelsLabelAndValue.value))
entries.append(.autoSaveInfo(presentationData.strings.Settings_SaveToCameraRollInfo))
-
-
+
+
let dataSaving = effectiveDataSaving(for: data.voiceCallSettings, autodownloadSettings: data.autodownloadSettings)
entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.ChatSettings_UseLessDataForCalls, dataSaving != .never))
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
-
+
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
@@ -643,7 +661,7 @@ private func dataAndStorageControllerEntries(context: AccountContext, state: Dat
entries.append(.sensitiveContent(presentationData.strings.Settings_SensitiveContent, contentSettingsConfiguration.sensitiveContentEnabled))
entries.append(.sensitiveContentInfo(presentationData.strings.Settings_SensitiveContentInfo))
}
-
+
let proxyValue: String
if let proxySettings = data.proxySettings, let activeServer = proxySettings.activeServer, proxySettings.enabled {
switch activeServer.connection {
@@ -657,32 +675,32 @@ private func dataAndStorageControllerEntries(context: AccountContext, state: Dat
}
entries.append(.connectionHeader(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_Title.uppercased()))
entries.append(.connectionProxy(presentationData.theme, presentationData.strings.SocksProxySetup_Title, proxyValue))
-
+
return entries
}
public func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndStorageEntryTag? = nil) -> ViewController {
let initialState = DataAndStorageControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
-
+
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentAgeVerificationImpl: ((@escaping () -> Void) -> Void)?
-
+
let actionsDisposable = DisposableSet()
-
+
//let cacheUsagePromise = Promise()
//cacheUsagePromise.set(cacheUsageStats(context: context))
-
+
let updateSensitiveContentDisposable = MetaDisposable()
actionsDisposable.add(updateSensitiveContentDisposable)
-
+
let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network)
|> map(Optional.init)
let contentSettingsConfiguration = Promise()
contentSettingsConfiguration.set(.single(nil)
|> then(updatedContentSettingsConfiguration))
-
+
struct UsageData: Equatable {
var network: Int64
var storage: Int64
@@ -694,56 +712,56 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
)
|> map { disk1, disk2, networkStats -> UsageData in
var network: Int64 = 0
-
+
var keys: [KeyPath] = []
-
+
keys.append(\.generic.cellular.outgoing)
keys.append(\.generic.cellular.incoming)
keys.append(\.generic.wifi.incoming)
keys.append(\.generic.wifi.outgoing)
-
+
keys.append(\.image.cellular.outgoing)
keys.append(\.image.cellular.incoming)
keys.append(\.image.wifi.incoming)
keys.append(\.image.wifi.outgoing)
-
+
keys.append(\.video.cellular.outgoing)
keys.append(\.video.cellular.incoming)
keys.append(\.video.wifi.incoming)
keys.append(\.video.wifi.outgoing)
-
+
keys.append(\.audio.cellular.outgoing)
keys.append(\.audio.cellular.incoming)
keys.append(\.audio.wifi.incoming)
keys.append(\.audio.wifi.outgoing)
-
+
keys.append(\.file.cellular.outgoing)
keys.append(\.file.cellular.incoming)
keys.append(\.file.wifi.incoming)
keys.append(\.file.wifi.outgoing)
-
+
keys.append(\.call.cellular.outgoing)
keys.append(\.call.cellular.incoming)
keys.append(\.call.wifi.incoming)
keys.append(\.call.wifi.outgoing)
-
+
keys.append(\.sticker.cellular.outgoing)
keys.append(\.sticker.cellular.incoming)
keys.append(\.sticker.wifi.incoming)
keys.append(\.sticker.wifi.outgoing)
-
+
keys.append(\.voiceMessage.cellular.outgoing)
keys.append(\.voiceMessage.cellular.incoming)
keys.append(\.voiceMessage.wifi.incoming)
keys.append(\.voiceMessage.wifi.outgoing)
-
+
for key in keys {
network += networkStats[keyPath: key]
}
-
+
return UsageData(network: network, storage: disk1 + disk2)
}
-
+
let dataAndStorageDataPromise = Promise()
dataAndStorageDataPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings, ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings, ApplicationSpecificSharedDataKeys.voiceCallSettings, ApplicationSpecificSharedDataKeys.mediaInputSettings, SharedDataKeys.proxySettings])
|> map { sharedData -> DataAndStorageData in
@@ -753,7 +771,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
} else {
automaticMediaDownloadSettings = .defaultSettings
}
-
+
var autodownloadSettings: AutodownloadSettings
if let value = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) {
autodownloadSettings = value
@@ -761,36 +779,36 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
} else {
autodownloadSettings = .defaultSettings
}
-
+
let generatedMediaStoreSettings: GeneratedMediaStoreSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings]?.get(GeneratedMediaStoreSettings.self) {
generatedMediaStoreSettings = value
} else {
generatedMediaStoreSettings = GeneratedMediaStoreSettings.defaultSettings
}
-
+
let mediaInputSettings: MediaInputSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaInputSettings]?.get(MediaInputSettings.self) {
mediaInputSettings = value
} else {
mediaInputSettings = MediaInputSettings.defaultSettings
}
-
+
let voiceCallSettings: VoiceCallSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) {
voiceCallSettings = value
} else {
voiceCallSettings = VoiceCallSettings.defaultSettings
}
-
+
var proxySettings: ProxySettings?
if let value = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
proxySettings = value
}
-
+
return DataAndStorageData(automaticMediaDownloadSettings: automaticMediaDownloadSettings, autodownloadSettings: autodownloadSettings, generatedMediaStoreSettings: generatedMediaStoreSettings, mediaInputSettings: mediaInputSettings, voiceCallSettings: voiceCallSettings, proxySettings: proxySettings)
})
-
+
let arguments = DataAndStorageControllerArguments(openStorageUsage: {
pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
return storageUsageExceptionsScreen(context: context, category: category)
@@ -806,7 +824,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
}
return automaticMediaDownloadSettings
}
-
+
let _ = (combineLatest(
accountNetworkUsageStats(account: context.account, reset: []),
mediaAutoDownloadSettings
@@ -814,18 +832,20 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|> take(1)
|> deliverOnMainQueue).start(next: { stats, mediaAutoDownloadSettings in
var stats = stats
-
+
if stats.resetWifiTimestamp == 0 {
var value = stat()
if stat(context.account.basePath, &value) == 0 {
stats.resetWifiTimestamp = Int32(value.st_ctimespec.tv_sec)
}
}
-
+
pushControllerImpl?(DataUsageScreen(context: context, stats: stats, mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: { isCellular in
return autodownloadMediaConnectionTypeController(context: context, connectionType: isCellular ? .cellular : .wifi)
}))
})
+ }, clearDeletedMessages: {
+ pushControllerImpl?(winterGramDeletedMessagesController(context: context))
}, openProxy: {
pushControllerImpl?(proxySettingsController(context: context))
}, openAutomaticDownloadConnectionType: { connectionType in
@@ -837,7 +857,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
ActionSheetTextItem(title: presentationData.strings.AutoDownloadSettings_ResetHelp),
ActionSheetButtonItem(title: presentationData.strings.AutoDownloadSettings_Reset, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
-
+
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
let defaultSettings = MediaAutoDownloadSettings.defaultSettings
@@ -910,12 +930,12 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
update()
}
})
-
+
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings))
|> map { entry -> MediaAutoSaveSettings in
return entry?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
}
-
+
let autosaveExceptionPeers: Signal<[EnginePeer.Id: EnginePeer?], NoError> = preferences
|> mapToSignal { mediaAutoSaveSettings -> Signal<[EnginePeer.Id: EnginePeer?], NoError> in
let peerIds = mediaAutoSaveSettings.exceptions.map(\.id)
@@ -926,7 +946,9 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
let sensitiveContent = Atomic(value: nil)
let canAdjustSensitiveContent = Atomic(value: nil)
-
+
+ let deletedMessagesSizeSignal = winterGramDeletedMessagesSize(postbox: context.account.postbox)
+
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
@@ -935,11 +957,12 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
contentSettingsConfiguration.get(),
preferences,
usageSignal,
- autosaveExceptionPeers
+ autosaveExceptionPeers,
+ deletedMessagesSizeSignal
)
- |> map { presentationData, state, dataAndStorageData, sharedData, contentSettingsConfiguration, mediaAutoSaveSettings, usageSignal, autosaveExceptionPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ |> map { presentationData, state, dataAndStorageData, sharedData, contentSettingsConfiguration, mediaAutoSaveSettings, usageSignal, autosaveExceptionPeers, deletedMessagesSize -> (ItemListControllerState, (ItemListNodeState, Any)) in
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings
-
+
let previousSensitiveContent = sensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled)
var animateChanges = false
if previousSensitiveContent != contentSettingsConfiguration?.sensitiveContentEnabled {
@@ -950,15 +973,15 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
let _ = canAdjustSensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled)
}
let showSensitiveContentSetting = canAdjustSensitiveContent.with { $0 } ?? false
-
+
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
- let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(context: context, state: state, data: dataAndStorageData, presentationData: presentationData, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings, showSensitiveContentSetting: showSensitiveContentSetting), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges)
-
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(context: context, state: state, data: dataAndStorageData, presentationData: presentationData, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, deletedMessagesSize: deletedMessagesSize, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings, showSensitiveContentSetting: showSensitiveContentSetting), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges)
+
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
-
+
let controller = ItemListController(context: context, state: signal)
pushControllerImpl = { [weak controller] c in
if let controller = controller {
@@ -976,7 +999,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
update()
})
}
-
+
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
@@ -990,6 +1013,6 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
}
}
}
-
+
return controller
}
diff --git a/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesCategoryItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesCategoryItem.swift
new file mode 100644
index 0000000000..b2dd4d9e03
--- /dev/null
+++ b/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesCategoryItem.swift
@@ -0,0 +1,280 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import ItemListUI
+import CheckNode
+
+// WinterGram: a Storage-Usage-style category row for the deleted-messages screen.
+// Shows a colored check-circle, title + percentage, and the size on the right.
+final class WinterGramDeletedMessagesCategoryItem: ListViewItem, ItemListItem {
+ let presentationData: ItemListPresentationData
+ let category: WinterGramDeletedMessageCategory
+ let color: UIColor
+ let title: String
+ let size: Int64
+ let fraction: Double
+ let checked: Bool
+ let isLast: Bool
+ let sectionId: ItemListSectionId
+ let toggle: () -> Void
+
+ init(presentationData: ItemListPresentationData, category: WinterGramDeletedMessageCategory, color: UIColor, title: String, size: Int64, fraction: Double, checked: Bool, isLast: Bool, sectionId: ItemListSectionId, toggle: @escaping () -> Void) {
+ self.presentationData = presentationData
+ self.category = category
+ self.color = color
+ self.title = title
+ self.size = size
+ self.fraction = fraction
+ self.checked = checked
+ self.isLast = isLast
+ self.sectionId = sectionId
+ self.toggle = toggle
+ }
+
+ func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
+ async {
+ let node = WinterGramDeletedMessagesCategoryItemNode()
+ let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ node.contentSize = layout.contentSize
+ node.insets = layout.insets
+ Queue.mainQueue().async {
+ completion(node, {
+ return (nil, { _ in apply() })
+ })
+ }
+ }
+ }
+
+ 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) {
+ Queue.mainQueue().async {
+ if let nodeValue = node() as? WinterGramDeletedMessagesCategoryItemNode {
+ let makeLayout = nodeValue.asyncLayout()
+ async {
+ let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ Queue.mainQueue().async {
+ completion(layout, { _ in
+ apply()
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+final class WinterGramDeletedMessagesCategoryItemNode: ListViewItemNode {
+ private let backgroundNode: ASDisplayNode
+ private let topStripeNode: ASDisplayNode
+ private let bottomStripeNode: ASDisplayNode
+ private let maskNode: ASImageNode
+ private let checkNode: CheckNode
+ private let titleNode: ImmediateTextNode
+ private let percentNode: ImmediateTextNode
+ private let sizeNode: ImmediateTextNode
+ private var separatorNode: ASDisplayNode?
+ private var tapGestureRecognizer: UITapGestureRecognizer?
+ private var tapAction: (() -> Void)?
+
+ init() {
+ self.backgroundNode = ASDisplayNode()
+ self.backgroundNode.isLayerBacked = true
+ self.topStripeNode = ASDisplayNode()
+ self.topStripeNode.isLayerBacked = true
+ self.bottomStripeNode = ASDisplayNode()
+ self.bottomStripeNode.isLayerBacked = true
+ self.maskNode = ASImageNode()
+
+ self.checkNode = CheckNode(theme: CheckNodeTheme(backgroundColor: .gray, strokeColor: .white, borderColor: .gray, overlayBorder: false, hasInset: false, hasShadow: false), content: .check(isRectangle: false))
+ self.titleNode = ImmediateTextNode()
+ self.titleNode.displaysAsynchronously = false
+ self.titleNode.isUserInteractionEnabled = false
+ self.percentNode = ImmediateTextNode()
+ self.percentNode.displaysAsynchronously = false
+ self.percentNode.isUserInteractionEnabled = false
+ self.sizeNode = ImmediateTextNode()
+ self.sizeNode.displaysAsynchronously = false
+ self.sizeNode.isUserInteractionEnabled = false
+
+ super.init(layerBacked: false)
+
+ self.addSubnode(self.checkNode)
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.percentNode)
+ self.addSubnode(self.sizeNode)
+ }
+
+ override func didLoad() {
+ super.didLoad()
+ let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapPressed))
+ self.tapGestureRecognizer = tap
+ self.view.addGestureRecognizer(tap)
+ }
+
+ @objc private func tapPressed() {
+ self.tapAction?()
+ }
+
+ func asyncLayout() -> (_ item: WinterGramDeletedMessagesCategoryItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
+ return { item, params, neighbors in
+ let height: CGFloat = 52.0
+ let contentSize = CGSize(width: params.width, height: height)
+ let insets = itemListNeighborsGroupedInsets(neighbors, params)
+ let separatorHeight = UIScreenPixel
+ let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
+ let layoutSize = layout.size
+
+ let theme = item.presentationData.theme
+
+ return (layout, { [weak self] in
+ guard let strongSelf = self else {
+ return
+ }
+
+ strongSelf.tapAction = item.toggle
+
+ strongSelf.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
+ strongSelf.topStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+ strongSelf.bottomStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+
+ if strongSelf.backgroundNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
+ }
+ if strongSelf.topStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
+ }
+ if strongSelf.bottomStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
+ }
+ if strongSelf.maskNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
+ }
+
+ let hasCorners = itemListHasRoundedBlockLayout(params)
+ switch neighbors.top {
+ case .sameSection(false):
+ strongSelf.topStripeNode.isHidden = true
+ default:
+ strongSelf.topStripeNode.isHidden = hasCorners
+ }
+ let bottomStripeInset: CGFloat
+ let bottomStripeOffset: CGFloat
+ switch neighbors.bottom {
+ case .sameSection(false):
+ bottomStripeInset = params.leftInset + 16.0
+ bottomStripeOffset = 0.0
+ default:
+ bottomStripeInset = 0.0
+ bottomStripeOffset = separatorHeight
+ }
+
+ strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layoutSize.width, height: contentSize.height))
+ strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
+ strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
+
+ strongSelf.maskNode.image = nil
+ if hasCorners {
+ let maskingCornerRadius: CGFloat = 10.0
+ if let cornersImage = generateStretchableFilledCircleImage(radius: maskingCornerRadius, color: .black) {
+ strongSelf.maskNode.image = cornersImage
+ }
+ }
+
+ let checkDiameter: CGFloat = 22.0
+ let checkFrame = CGRect(origin: CGPoint(x: 20.0, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter))
+ strongSelf.checkNode.frame = checkFrame
+ strongSelf.checkNode.theme = CheckNodeTheme(
+ backgroundColor: item.color,
+ strokeColor: theme.list.itemCheckColors.foregroundColor,
+ borderColor: theme.list.itemCheckColors.strokeColor,
+ overlayBorder: false,
+ hasInset: false,
+ hasShadow: false
+ )
+ strongSelf.checkNode.setSelected(item.checked, animated: false)
+
+ let sizeFormatting = DataSizeStringFormatting(strings: item.presentationData.strings, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
+ let sizeString = dataSizeString(item.size, formatting: sizeFormatting)
+ strongSelf.sizeNode.attributedText = NSAttributedString(string: sizeString, font: Font.regular(17.0), textColor: theme.list.itemSecondaryTextColor)
+ let sizeSize = strongSelf.sizeNode.updateLayout(CGSize(width: params.width - 100.0, height: 44.0))
+
+ let percentString: String
+ if item.fraction > 0.0 {
+ let value = floor(item.fraction * 100.0 * 10.0) / 10.0
+ if value < 0.1 {
+ percentString = "<0.1%"
+ } else if abs(Double(Int(value)) - value) < 0.001 {
+ percentString = "\(Int(value))%"
+ } else {
+ percentString = "\(value)%"
+ }
+ } else {
+ percentString = ""
+ }
+ strongSelf.percentNode.attributedText = NSAttributedString(string: percentString, font: Font.regular(17.0), textColor: theme.list.itemSecondaryTextColor)
+ let percentSize = strongSelf.percentNode.updateLayout(CGSize(width: params.width - 100.0, height: 44.0))
+
+ strongSelf.titleNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
+
+ let sizeFrame = CGRect(origin: CGPoint(x: params.width - 16.0 - sizeSize.width, y: floor((height - sizeSize.height) / 2.0)), size: sizeSize)
+ let percentFrame = CGRect(origin: CGPoint(x: sizeFrame.minX - 8.0 - percentSize.width, y: floor((height - percentSize.height) / 2.0)), size: percentSize)
+ let titleMaxWidth = max(0.0, percentFrame.minX - 8.0 - 62.0)
+ let titleSize = strongSelf.titleNode.updateLayout(CGSize(width: titleMaxWidth, height: 44.0))
+ let titleFrame = CGRect(origin: CGPoint(x: 62.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
+
+ strongSelf.titleNode.frame = titleFrame
+ strongSelf.percentNode.frame = percentFrame
+ strongSelf.sizeNode.frame = sizeFrame
+
+ if item.isLast {
+ if let separatorNode = strongSelf.separatorNode {
+ separatorNode.isHidden = true
+ }
+ } else {
+ let separatorNode: ASDisplayNode
+ if let current = strongSelf.separatorNode {
+ separatorNode = current
+ } else {
+ separatorNode = ASDisplayNode()
+ separatorNode.isLayerBacked = true
+ strongSelf.separatorNode = separatorNode
+ strongSelf.insertSubnode(separatorNode, aboveSubnode: strongSelf.backgroundNode)
+ }
+ separatorNode.isHidden = false
+ separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+ separatorNode.frame = CGRect(origin: CGPoint(x: 62.0, y: height), size: CGSize(width: params.width - 62.0, height: UIScreenPixel))
+ }
+ })
+ }
+ }
+}
+
+// Expose a factory so the controller can use the custom row without knowing the private types.
+func winterGramDeletedMessagesCategoryItem(
+ presentationData: ItemListPresentationData,
+ category: WinterGramDeletedMessageCategory,
+ color: UIColor,
+ title: String,
+ size: Int64,
+ fraction: Double,
+ checked: Bool,
+ isLast: Bool,
+ sectionId: ItemListSectionId,
+ toggle: @escaping () -> Void
+) -> ListViewItem {
+ return WinterGramDeletedMessagesCategoryItem(
+ presentationData: presentationData,
+ category: category,
+ color: color,
+ title: title,
+ size: size,
+ fraction: fraction,
+ checked: checked,
+ isLast: isLast,
+ sectionId: sectionId,
+ toggle: toggle
+ )
+}
diff --git a/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesController.swift b/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesController.swift
new file mode 100644
index 0000000000..e01cd40e4e
--- /dev/null
+++ b/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesController.swift
@@ -0,0 +1,558 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import TelegramUIPreferences
+import ItemListUI
+import ItemListPeerActionItem
+import ItemListPeerItem
+import AccountContext
+import TelegramStringFormatting
+import UndoUI
+import ComponentFlow
+import StorageUsageScreen
+
+private func generatePieChartImage(size: CGSize, values: [(color: UIColor, fraction: CGFloat)], theme: PresentationTheme) -> UIImage? {
+ return generateImage(size, contextGenerator: { targetSize, context in
+ context.clear(CGRect(origin: .zero, size: targetSize))
+ let center = CGPoint(x: targetSize.width / 2.0, y: targetSize.height / 2.0)
+ let outerRadius = min(targetSize.width, targetSize.height) / 2.0 - 2.0
+ let innerRadius = outerRadius * 0.52
+ let separatorAngle: CGFloat = 0.03
+ var startAngle: CGFloat = -CGFloat.pi / 2.0
+ let total = values.reduce(0.0) { $0 + max(0.0, $1.fraction) }
+ guard total > 0.0 else {
+ let emptyColor = theme.list.itemAccentColor.withAlphaComponent(0.25)
+ context.setFillColor(emptyColor.cgColor)
+ context.fillEllipse(in: CGRect(x: center.x - outerRadius, y: center.y - outerRadius, width: outerRadius * 2.0, height: outerRadius * 2.0))
+ context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
+ context.fillEllipse(in: CGRect(x: center.x - innerRadius, y: center.y - innerRadius, width: innerRadius * 2.0, height: innerRadius * 2.0))
+ return
+ }
+ for (color, fraction) in values {
+ let rawSweep = (fraction / total) * CGFloat.pi * 2.0
+ let sweep = max(0.0, rawSweep - separatorAngle)
+ let endAngle = startAngle + sweep
+ let path = CGMutablePath()
+ path.addArc(center: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
+ path.addArc(center: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
+ path.closeSubpath()
+ context.addPath(path)
+ context.setFillColor(color.cgColor)
+ context.fillPath()
+ startAngle += rawSweep
+ }
+ }, opaque: false)
+}
+
+private let categoryColors: [WinterGramDeletedMessageCategory: UIColor] = [
+ .text: UIColor(rgb: 0x34C759),
+ .photo: UIColor(rgb: 0x007AFF),
+ .video: UIColor(rgb: 0xFF2D55),
+ .voice: UIColor(rgb: 0xFF9500),
+ .videoMessage: UIColor(rgb: 0xAF52DE),
+ .music: UIColor(rgb: 0x5856D6),
+ .sticker: UIColor(rgb: 0xFFCC00),
+ .other: UIColor(rgb: 0x8E8E93)
+]
+
+private func categoryTitle(_ category: WinterGramDeletedMessageCategory, _ strings: PresentationStrings) -> String {
+ switch category {
+ case .text:
+ return strings.WinterGram_DeletedMessages_Text
+ case .photo:
+ return strings.WinterGram_DeletedMessages_Photos
+ case .video:
+ return strings.WinterGram_DeletedMessages_Videos
+ case .voice:
+ return strings.WinterGram_DeletedMessages_Voice
+ case .videoMessage:
+ return strings.WinterGram_DeletedMessages_VideoMessages
+ case .music:
+ return strings.WinterGram_DeletedMessages_Music
+ case .sticker:
+ return strings.WinterGram_DeletedMessages_Stickers
+ case .other:
+ return strings.WinterGram_DeletedMessages_Other
+ }
+}
+
+private final class WinterGramDeletedMessagesChartItem: ListViewItem, ItemListItem {
+ let presentationData: ItemListPresentationData
+ let chartData: PieChartComponent.ChartData
+ let totalText: String
+ let sectionId: ItemListSectionId
+
+ init(presentationData: ItemListPresentationData, chartData: PieChartComponent.ChartData, totalText: String, sectionId: ItemListSectionId) {
+ self.presentationData = presentationData
+ self.chartData = chartData
+ self.totalText = totalText
+ self.sectionId = sectionId
+ }
+
+ func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
+ async {
+ let node = WinterGramDeletedMessagesChartItemNode()
+ let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ node.contentSize = layout.contentSize
+ node.insets = layout.insets
+ Queue.mainQueue().async {
+ completion(node, {
+ return (nil, { _ in apply() })
+ })
+ }
+ }
+ }
+
+ 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) {
+ Queue.mainQueue().async {
+ if let nodeValue = node() as? WinterGramDeletedMessagesChartItemNode {
+ let makeLayout = nodeValue.asyncLayout()
+ async {
+ let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ Queue.mainQueue().async {
+ completion(layout, { _ in
+ apply()
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+private final class WinterGramDeletedMessagesChartItemNode: ListViewItemNode {
+ private let backgroundNode: ASDisplayNode
+ private let topStripeNode: ASDisplayNode
+ private let bottomStripeNode: ASDisplayNode
+ private let maskNode: ASImageNode
+ private let chartHost: ComponentHostView
+ private let totalLabelNode: ImmediateTextNode
+
+ init() {
+ self.backgroundNode = ASDisplayNode()
+ self.backgroundNode.isLayerBacked = true
+ self.topStripeNode = ASDisplayNode()
+ self.topStripeNode.isLayerBacked = true
+ self.bottomStripeNode = ASDisplayNode()
+ self.bottomStripeNode.isLayerBacked = true
+ self.maskNode = ASImageNode()
+ self.chartHost = ComponentHostView()
+ self.totalLabelNode = ImmediateTextNode()
+ self.totalLabelNode.displaysAsynchronously = false
+ self.totalLabelNode.isUserInteractionEnabled = false
+
+ super.init(layerBacked: false)
+
+ self.addSubnode(self.totalLabelNode)
+ }
+
+ func asyncLayout() -> (_ item: WinterGramDeletedMessagesChartItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
+ return { item, params, neighbors in
+ let imageSize = CGSize(width: 240.0, height: 240.0)
+ let contentSize = CGSize(width: params.width, height: imageSize.height + 24.0)
+ let insets = itemListNeighborsGroupedInsets(neighbors, params)
+ let separatorHeight = UIScreenPixel
+ let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
+ let layoutSize = layout.size
+
+ let theme = item.presentationData.theme
+
+ return (layout, { [weak self] in
+ guard let strongSelf = self else {
+ return
+ }
+
+ strongSelf.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
+ strongSelf.topStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+ strongSelf.bottomStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+
+ if strongSelf.backgroundNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
+ }
+ if strongSelf.topStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
+ }
+ if strongSelf.bottomStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
+ }
+ if strongSelf.maskNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
+ }
+
+ let hasCorners = itemListHasRoundedBlockLayout(params)
+ var hasTopCorners = false
+ var hasBottomCorners = false
+ switch neighbors.top {
+ case .sameSection(false):
+ strongSelf.topStripeNode.isHidden = true
+ default:
+ hasTopCorners = true
+ strongSelf.topStripeNode.isHidden = hasCorners
+ }
+ let bottomStripeInset: CGFloat
+ let bottomStripeOffset: CGFloat
+ switch neighbors.bottom {
+ case .sameSection(false):
+ bottomStripeInset = params.leftInset + 16.0
+ bottomStripeOffset = -separatorHeight
+ strongSelf.bottomStripeNode.isHidden = false
+ default:
+ bottomStripeInset = 0.0
+ bottomStripeOffset = 0.0
+ hasBottomCorners = true
+ strongSelf.bottomStripeNode.isHidden = hasCorners
+ }
+
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : 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)
+ strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
+ strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
+
+ if strongSelf.chartHost.superview == nil {
+ strongSelf.view.addSubview(strongSelf.chartHost)
+ }
+ // Keep the centre total label above the pie.
+ strongSelf.view.bringSubviewToFront(strongSelf.totalLabelNode.view)
+ let chartSize = strongSelf.chartHost.update(
+ transition: .immediate,
+ component: AnyComponent(PieChartComponent(
+ theme: theme,
+ strings: item.presentationData.strings,
+ emptyColor: theme.list.itemAccentColor.withAlphaComponent(0.25),
+ chartData: item.chartData
+ )),
+ environment: {},
+ containerSize: imageSize
+ )
+ let chartFrame = CGRect(origin: CGPoint(x: floor((params.width - chartSize.width) / 2.0), y: 12.0), size: chartSize)
+ strongSelf.chartHost.frame = chartFrame
+
+ // Total size in the donut centre, matching the native Storage Usage screen.
+ strongSelf.totalLabelNode.attributedText = NSAttributedString(string: item.totalText, font: Font.semibold(20.0), textColor: theme.list.itemPrimaryTextColor)
+ let totalLabelSize = strongSelf.totalLabelNode.updateLayout(CGSize(width: chartFrame.width, height: 44.0))
+ strongSelf.totalLabelNode.frame = CGRect(origin: CGPoint(x: chartFrame.minX + floor((chartFrame.width - totalLabelSize.width) / 2.0), y: chartFrame.minY + floor((chartFrame.height - totalLabelSize.height) / 2.0)), size: totalLabelSize)
+ })
+ }
+ }
+
+ override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
+ self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
+ }
+
+ override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
+ self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
+ }
+}
+
+private enum WinterGramDeletedMessagesControllerSection: Int32 {
+ case overview
+ case chart
+ case categories
+ case topChats
+ case delete
+}
+
+private enum WinterGramDeletedMessagesControllerEntry: ItemListNodeEntry {
+ case overviewHeader(PresentationTheme, String)
+ case overviewInfo(PresentationTheme, String)
+ case chart(PresentationTheme, PieChartComponent.ChartData, String)
+ case categoryHeader(PresentationTheme, String)
+ case category(PresentationTheme, WinterGramDeletedMessageCategory, Int64, Int, Bool, Bool, Double)
+ case topChatsHeader(PresentationTheme, String)
+ case topChat(PresentationTheme, EnginePeer, Int, Int64, Int32)
+ case delete(PresentationTheme, String, Bool)
+
+ var section: ItemListSectionId {
+ switch self {
+ case .overviewHeader, .overviewInfo:
+ return WinterGramDeletedMessagesControllerSection.overview.rawValue
+ case .chart:
+ return WinterGramDeletedMessagesControllerSection.chart.rawValue
+ case .categoryHeader, .category:
+ return WinterGramDeletedMessagesControllerSection.categories.rawValue
+ case .topChatsHeader, .topChat:
+ return WinterGramDeletedMessagesControllerSection.topChats.rawValue
+ case .delete:
+ return WinterGramDeletedMessagesControllerSection.delete.rawValue
+ }
+ }
+
+ var stableId: Int32 {
+ switch self {
+ case .overviewHeader:
+ return 0
+ case .overviewInfo:
+ return 1
+ case .chart:
+ return 2
+ case .categoryHeader:
+ return 3
+ case let .category(_, category, _, _, _, _, _):
+ return 10 + category.rawValue
+ case .topChatsHeader:
+ return 200
+ case let .topChat(_, _, _, _, index):
+ return 210 + index
+ case .delete:
+ return 1000
+ }
+ }
+
+ static func ==(lhs: WinterGramDeletedMessagesControllerEntry, rhs: WinterGramDeletedMessagesControllerEntry) -> Bool {
+ switch lhs {
+ case let .overviewHeader(lhsTheme, lhsText):
+ if case let .overviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .overviewInfo(lhsTheme, lhsText):
+ if case let .overviewInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .chart(lhsTheme, lhsData, lhsTotal):
+ if case let .chart(rhsTheme, rhsData, rhsTotal) = rhs, lhsTheme === rhsTheme, lhsData == rhsData, lhsTotal == rhsTotal {
+ return true
+ } else {
+ return false
+ }
+ case let .categoryHeader(lhsTheme, lhsText):
+ if case let .categoryHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .category(lhsTheme, lhsCategory, lhsSize, lhsCount, lhsChecked, lhsIsLast, lhsFraction):
+ if case let .category(rhsTheme, rhsCategory, rhsSize, rhsCount, rhsChecked, rhsIsLast, rhsFraction) = rhs, lhsTheme === rhsTheme, lhsCategory == rhsCategory, lhsSize == rhsSize, lhsCount == rhsCount, lhsChecked == rhsChecked, lhsIsLast == rhsIsLast, lhsFraction == rhsFraction {
+ return true
+ } else {
+ return false
+ }
+ case let .topChatsHeader(lhsTheme, lhsText):
+ if case let .topChatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .topChat(lhsTheme, lhsPeer, lhsCount, lhsSize, lhsIndex):
+ if case let .topChat(rhsTheme, rhsPeer, rhsCount, rhsSize, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsCount == rhsCount, lhsSize == rhsSize, lhsIndex == rhsIndex {
+ return true
+ } else {
+ return false
+ }
+ case let .delete(lhsTheme, lhsText, lhsEnabled):
+ if case let .delete(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
+ return true
+ } else {
+ return false
+ }
+ }
+ }
+
+ static func <(lhs: WinterGramDeletedMessagesControllerEntry, rhs: WinterGramDeletedMessagesControllerEntry) -> Bool {
+ return lhs.stableId < rhs.stableId
+ }
+
+ func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
+ let arguments = arguments as! WinterGramDeletedMessagesControllerArguments
+ switch self {
+ case let .overviewHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .overviewInfo(_, text):
+ return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, style: .blocks)
+ case let .chart(_, chartData, totalText):
+ return WinterGramDeletedMessagesChartItem(presentationData: presentationData, chartData: chartData, totalText: totalText, sectionId: self.section)
+ case let .categoryHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .category(_, category, size, _, checked, isLast, fraction):
+ let title = categoryTitle(category, presentationData.strings)
+ return winterGramDeletedMessagesCategoryItem(
+ presentationData: presentationData,
+ category: category,
+ color: categoryColors[category] ?? presentationData.theme.list.itemAccentColor,
+ title: title,
+ size: size,
+ fraction: fraction,
+ checked: checked,
+ isLast: isLast,
+ sectionId: self.section,
+ toggle: {
+ arguments.toggleCategory(category)
+ }
+ )
+ case let .topChatsHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .topChat(_, peer, count, size, _):
+ let sizeFormatting = DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
+ let subtitle = "\(count) • \(dataSizeString(size, formatting: sizeFormatting))"
+ return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(subtitle, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), switchValue: nil, enabled: true, selectable: false, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
+ case let .delete(_, text, enabled):
+ return ItemListPeerActionItem(presentationData: presentationData, style: .blocks, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, color: enabled ? .destructive : .disabled, editing: false, action: enabled ? {
+ arguments.deleteSelected()
+ } : nil)
+ }
+ }
+}
+
+private func generateFilledCircleImage(diameter: CGFloat, color: UIColor) -> UIImage? {
+ return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
+ context.clear(CGRect(origin: .zero, size: size))
+ context.setFillColor(color.cgColor)
+ context.fillEllipse(in: CGRect(origin: .zero, size: size))
+ }, opaque: false)
+}
+
+private final class WinterGramDeletedMessagesControllerArguments {
+ let context: AccountContext
+ let toggleCategory: (WinterGramDeletedMessageCategory) -> Void
+ let deleteSelected: () -> Void
+
+ init(context: AccountContext, toggleCategory: @escaping (WinterGramDeletedMessageCategory) -> Void, deleteSelected: @escaping () -> Void) {
+ self.context = context
+ self.toggleCategory = toggleCategory
+ self.deleteSelected = deleteSelected
+ }
+}
+
+private func winterGramDeletedMessagesControllerEntries(stats: WinterGramDeletedMessagesStats, topPeers: [EnginePeer], selectedCategories: Set, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, theme: PresentationTheme) -> [WinterGramDeletedMessagesControllerEntry] {
+ let sizeFormatting = DataSizeStringFormatting(strings: strings, decimalSeparator: dateTimeFormat.decimalSeparator)
+
+ var entries: [WinterGramDeletedMessagesControllerEntry] = []
+
+ entries.append(.overviewHeader(theme, strings.WinterGram_DeletedMessages_Title.uppercased()))
+ entries.append(.overviewInfo(theme, "\(strings.WinterGram_DeletedMessages_Total): \(stats.totalCount) • \(dataSizeString(stats.totalSize, formatting: sizeFormatting))"))
+
+ let chartTotal = stats.categories.reduce(Int64(0)) { $0 + max(0, $1.size) }
+ var chartItems: [PieChartComponent.ChartData.Item] = []
+ for stat in stats.categories where stat.size > 0 {
+ let color = categoryColors[stat.category] ?? theme.list.itemAccentColor
+ let fraction = chartTotal > 0 ? Double(stat.size) / Double(chartTotal) : 0.0
+ chartItems.append(PieChartComponent.ChartData.Item(
+ id: AnyHashable(stat.category),
+ displayValue: fraction,
+ displaySize: stat.size,
+ value: fraction,
+ color: color,
+ particle: nil,
+ title: categoryTitle(stat.category, strings),
+ mergeable: false,
+ mergeFactor: 1.0
+ ))
+ }
+ let chartTotalText = dataSizeString(chartTotal, formatting: sizeFormatting)
+ entries.append(.chart(theme, PieChartComponent.ChartData(items: chartItems), chartTotalText))
+
+ entries.append(.categoryHeader(theme, strings.WinterGram_DeletedMessages_SelectTypes.uppercased()))
+ let visibleCategories = stats.categories.filter { $0.count > 0 }
+ for (index, stat) in visibleCategories.enumerated() {
+ let fraction = chartTotal > 0 ? Double(stat.size) / Double(chartTotal) : 0.0
+ entries.append(.category(theme, stat.category, stat.size, stat.count, selectedCategories.contains(stat.category), index == visibleCategories.count - 1, fraction))
+ }
+
+ if !stats.topChats.isEmpty {
+ entries.append(.topChatsHeader(theme, strings.WinterGram_DeletedMessages_TopChats.uppercased()))
+ var topChatIndex: Int32 = 0
+ for stat in stats.topChats {
+ if let peer = topPeers.first(where: { $0.id == EnginePeer.Id(stat.peerId) }) {
+ entries.append(.topChat(theme, peer, stat.count, stat.size, topChatIndex))
+ topChatIndex += 1
+ }
+ }
+ }
+
+ let canDelete = !selectedCategories.isEmpty
+ entries.append(.delete(theme, strings.WinterGram_DeletedMessages_DeleteSelected, canDelete))
+
+ return entries
+}
+
+public func winterGramDeletedMessagesController(context: AccountContext) -> ViewController {
+ var presentControllerImpl: ((ViewController, Any?) -> Void)?
+
+ let selectedCategories = Atomic>(value: Set(WinterGramDeletedMessageCategory.allCases))
+ let selectedCategoriesPromise = ValuePromise>(Set(WinterGramDeletedMessageCategory.allCases), ignoreRepeated: true)
+
+ let arguments = WinterGramDeletedMessagesControllerArguments(context: context, toggleCategory: { category in
+ let selected = selectedCategories.modify { selected in
+ var selected = selected
+ if selected.contains(category) {
+ selected.remove(category)
+ } else {
+ selected.insert(category)
+ }
+ return selected
+ }
+ selectedCategoriesPromise.set(selected)
+ }, deleteSelected: {
+ let selected = selectedCategories.with { $0 }
+ guard !selected.isEmpty else {
+ return
+ }
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ let actionSheet = ActionSheetController(presentationData: presentationData)
+ actionSheet.setItemGroups([ActionSheetItemGroup(items: [
+ ActionSheetTextItem(title: presentationData.strings.WinterGram_DeletedMessages_ConfirmDelete),
+ ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
+ actionSheet?.dismissAnimated()
+ let _ = (winterGramClearDeletedMessages(postbox: context.account.postbox, categories: selected)
+ |> deliverOnMainQueue).start(next: { freedSize in
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ let sizeFormatting = DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
+ presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.WinterGram_DeletedMessages_Deleted(dataSizeString(freedSize, formatting: sizeFormatting)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), nil)
+ selectedCategoriesPromise.set(Set(WinterGramDeletedMessageCategory.allCases))
+ let _ = selectedCategories.swap(Set(WinterGramDeletedMessageCategory.allCases))
+ })
+ })
+ ]), ActionSheetItemGroup(items: [
+ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
+ actionSheet?.dismissAnimated()
+ })
+ ])])
+ presentControllerImpl?(actionSheet, nil)
+ })
+
+ let statsSignal = winterGramDeletedMessagesStats(postbox: context.account.postbox)
+ let topPeersSignal = statsSignal
+ |> map { stats -> [EnginePeer.Id] in
+ return stats.topChats.map { EnginePeer.Id($0.peerId) }
+ }
+ |> distinctUntilChanged
+ |> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
+ guard !peerIds.isEmpty else {
+ return .single([])
+ }
+ return context.engine.data.subscribe(
+ EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
+ )
+ |> map { peerMap -> [EnginePeer] in
+ return peerMap.values.compactMap { $0 }
+ }
+ }
+
+ let signal = combineLatest(queue: .mainQueue(),
+ context.sharedContext.presentationData,
+ statsSignal,
+ selectedCategoriesPromise.get(),
+ topPeersSignal
+ )
+ |> map { presentationData, stats, selectedCategories, topPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WinterGram_DeletedMessages_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: winterGramDeletedMessagesControllerEntries(stats: stats, topPeers: topPeers, selectedCategories: selectedCategories, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, theme: presentationData.theme), style: .blocks, animateChanges: true)
+ return (controllerState, (listState, arguments))
+ }
+
+ let controller = ItemListController(context: context, state: signal)
+ presentControllerImpl = { [weak controller] c, a in
+ controller?.present(c, in: .window(.root), with: a)
+ }
+
+ return controller
+}
diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift
index 3db095dc8c..470861bb96 100644
--- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift
+++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift
@@ -13,7 +13,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
-
+
let lineWidth: CGFloat
if selected {
var accentColor = theme.list.itemAccentColor
@@ -26,7 +26,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0 - UIScreenPixel
}
-
+
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
@@ -36,7 +36,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec
class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
-
+
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
@@ -45,7 +45,7 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
let currentIconName: String?
let updated: (PresentationAppIcon) -> Void
let tag: ItemListItemTag?
-
+
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle, sectionId: ItemListSectionId, icons: [PresentationAppIcon], isPremium: Bool, currentIconName: String?, updated: @escaping (PresentationAppIcon) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.strings = strings
@@ -57,15 +57,15 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
self.tag = tag
self.sectionId = sectionId
}
-
+
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAppIconItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
-
+
node.contentSize = layout.contentSize
node.insets = layout.insets
-
+
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
@@ -73,12 +73,12 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
}
}
}
-
+
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) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsAppIconItemNode {
let makeLayout = nodeValue.asyncLayout()
-
+
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
@@ -95,49 +95,52 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
private let badgeSize = CGSize(width: 24.0, height: 24.0)
private let badgeStrokeSize: CGFloat = 2.0
+private let appIconGridIconSize = CGSize(width: 70.0, height: 70.0)
+private let appIconGridNodeSize = CGSize(width: 82.0, height: 112.0)
+
private final class ThemeSettingsAppIconNode : ASDisplayNode {
private let iconNode: ASImageNode
private let overlayNode: ASImageNode
fileprivate let lockNode: ASImageNode
private let textNode: ImmediateTextNode
private var action: (() -> Void)?
-
+
private let activateAreaNode: AccessibilityAreaNode
-
+
private var locked = false
-
+
override init() {
self.iconNode = ASImageNode()
self.iconNode.clipsToBounds = true
- self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
+ self.iconNode.frame = CGRect(origin: CGPoint(), size: appIconGridIconSize)
self.iconNode.isLayerBacked = true
- self.iconNode.cornerRadius = 15.0
-
+ self.iconNode.cornerRadius = 17.0
+
self.overlayNode = ASImageNode()
- self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
+ self.overlayNode.frame = CGRect(origin: CGPoint(), size: appIconGridIconSize)
self.overlayNode.isLayerBacked = true
-
+
self.lockNode = ASImageNode()
self.lockNode.contentMode = .scaleAspectFit
self.lockNode.displaysAsynchronously = false
self.lockNode.isUserInteractionEnabled = false
-
+
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
-
+
self.activateAreaNode = AccessibilityAreaNode()
self.activateAreaNode.accessibilityTraits = [.button]
-
+
super.init()
-
+
self.addSubnode(self.iconNode)
self.addSubnode(self.overlayNode)
self.addSubnode(self.textNode)
self.addSubnode(self.lockNode)
self.addSubnode(self.activateAreaNode)
}
-
+
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, locked: Bool, color: UIColor, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
self.locked = locked
self.iconNode.image = icon
@@ -147,52 +150,90 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode {
self.action = {
action()
}
-
+
self.activateAreaNode.accessibilityLabel = title.string
if locked {
self.activateAreaNode.accessibilityTraits = [.button, .notEnabled]
} else {
self.activateAreaNode.accessibilityTraits = [.button]
}
-
+
self.setNeedsLayout()
}
-
+
override func didLoad() {
super.didLoad()
-
+
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
-
+
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?()
}
}
-
+
override func layout() {
super.layout()
-
+
let bounds = self.bounds
- let iconSize = CGSize(width: 63.0, height: 63.0)
-
- self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize)
+ let iconSize = appIconGridIconSize
+
+ self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 14.0), size: iconSize)
self.overlayNode.frame = self.iconNode.frame
-
+
let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width + 8.0, height: bounds.size.height))
- let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize)
+ let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 91.0), size: textSize)
self.textNode.frame = textFrame
-
+
let badgeFinalSize = CGSize(width: badgeSize.width + badgeStrokeSize * 2.0, height: badgeSize.height + badgeStrokeSize * 2.0)
self.lockNode.frame = CGRect(x: bounds.width - 24.0, y: 4.0, width: badgeFinalSize.width, height: badgeFinalSize.height)
-
+
self.activateAreaNode.frame = bounds
}
}
-private let textFont = Font.regular(12.0)
-private let selectedTextFont = Font.medium(12.0)
+private let textFont = Font.regular(13.0)
+private let selectedTextFont = Font.medium(13.0)
+
+private func winterGramAssetDisplayTitle(_ rawName: String, prefixes: [String]) -> String? {
+ var name = rawName.components(separatedBy: "/").last ?? rawName
+ if let dotIndex = name.lastIndex(of: ".") {
+ name = String(name[.. String in
+ let lowercased = word.lowercased()
+ if lowercased == "wintergram" {
+ return "WinterGram"
+ }
+ if lowercased == "ios" {
+ return "iOS"
+ }
+ return lowercased.prefix(1).uppercased() + String(lowercased.dropFirst())
+ }
+
+ return words.isEmpty ? nil : words.joined(separator: " ")
+}
+
+private func winterGramIconTitle(_ name: String) -> String? {
+ return winterGramAssetDisplayTitle(name, prefixes: [
+ "icon-app-",
+ "app-icon-",
+ "WinterGram"
+ ])
+}
class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
@@ -200,103 +241,103 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
-
+
private let containerNode: ASDisplayNode
private var nodes: [ThemeSettingsAppIconNode] = []
-
+
private var item: ThemeSettingsAppIconItem?
private var layoutParams: ListViewItemLayoutParams?
-
+
var tag: ItemListItemTag? {
return self.item?.tag
}
-
+
private var lockImage: UIImage?
-
+
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
-
+
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
-
+
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
-
+
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
-
+
self.maskNode = ASImageNode()
-
+
self.containerNode = ASDisplayNode()
-
+
super.init(layerBacked: false)
-
+
self.addSubnode(self.containerNode)
}
-
+
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
-
+
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
-
+
func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
-
- let nodeSize = CGSize(width: 74.0, height: 102.0)
- let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 12.0
-
+
+ let nodeSize = appIconGridNodeSize
+ let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 14.0
+
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
-
+
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
-
+
return (layout, { [weak self] in
if let strongSelf = self {
let previousItem = strongSelf.item
strongSelf.item = item
strongSelf.layoutParams = params
-
+
if previousItem?.theme !== item.theme {
strongSelf.lockImage = generateImage(CGSize(width: badgeSize.width + badgeStrokeSize, height: badgeSize.height + badgeStrokeSize), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
-
+
context.setFillColor(item.theme.list.itemBlocksBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
-
+
context.addEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: badgeStrokeSize, dy: badgeStrokeSize))
context.clip()
-
+
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
-
+
if let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: .white) {
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}
-
+
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor
-
+
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
@@ -309,7 +350,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
-
+
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
@@ -333,24 +374,24 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
-
+
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.highlightNode.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)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
-
+
strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height))
-
+
let sideInset: CGFloat = 8.0
let spacing: CGFloat = floorToScreenPixels((params.width - sideInset * 2.0 - params.leftInset - params.rightInset - nodeSize.width * 4.0) / 3.0)
let verticalSpacing: CGFloat = 0.0
-
+
var x: CGFloat = sideInset
var y: CGFloat = 0.0
-
+
var i = 0
for icon in item.icons {
if i > 0 && i % 4 == 0 {
@@ -359,7 +400,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
}
let nodeFrame = CGRect(x: x, y: y, width: nodeSize.width, height: nodeSize.height)
x += nodeSize.width + spacing
-
+
let imageNode: ThemeSettingsAppIconNode
if strongSelf.nodes.count > i {
imageNode = strongSelf.nodes[i]
@@ -369,7 +410,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.containerNode.addSubnode(imageNode)
}
imageNode.lockNode.image = strongSelf.lockImage
-
+
if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) {
let selected = icon.name == item.currentIconName
@@ -403,29 +444,33 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
case "PremiumTurbo":
name = item.strings.Appearance_AppIconTurbo
default:
- name = icon.name
+ name = winterGramIconTitle(icon.name) ?? icon.name
}
-
+
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: {
item.updated(icon)
})
}
-
+
imageNode.frame = nodeFrame
-
+
i += 1
}
+
+ while strongSelf.nodes.count > i {
+ let node = strongSelf.nodes.removeLast()
+ node.removeFromSupernode()
+ }
}
})
}
}
-
+
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
-
+
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
-
diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift
index 58e3acebfc..64ac5ee99d 100644
--- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift
+++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift
@@ -10,6 +10,7 @@ import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
+import AvatarNode
struct ChatPreviewMessageItem: Equatable {
static func == (lhs: ChatPreviewMessageItem, rhs: ChatPreviewMessageItem) -> Bool {
@@ -27,16 +28,20 @@ struct ChatPreviewMessageItem: Equatable {
if lhs.nameColor != rhs.nameColor {
return false
}
+ if lhs.photo != rhs.photo {
+ return false
+ }
if lhs.backgroundEmojiId != rhs.backgroundEmojiId {
return false
}
return true
}
-
+
let outgoing: Bool
let reply: (String, String)?
let text: String
let nameColor: PeerColor
+ var photo: [TelegramMediaImageRepresentation] = []
let backgroundEmojiId: Int64?
}
@@ -53,8 +58,10 @@ class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem {
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let messageItems: [ChatPreviewMessageItem]
-
- init(context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem]) {
+ let avatarCornerRadius: Int32
+ let avatarPeer: EnginePeer?
+
+ init(context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem], avatarCornerRadius: Int32 = currentWinterGramSettings.avatarCornerRadius, avatarPeer: EnginePeer? = nil) {
self.context = context
self.systemStyle = systemStyle
self.theme = theme
@@ -67,16 +74,18 @@ class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem {
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.messageItems = messageItems
+ self.avatarCornerRadius = avatarCornerRadius
+ self.avatarPeer = avatarPeer
}
-
+
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsChatPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
-
+
node.contentSize = layout.contentSize
node.insets = layout.insets
-
+
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
@@ -84,12 +93,12 @@ class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem {
}
}
}
-
+
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) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsChatPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
-
+
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
@@ -108,72 +117,107 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
-
+ private let avatarNode: AvatarNode
+
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
-
+
private var item: ThemeSettingsChatPreviewItem?
private var finalImage = true
-
+
private let disposable = MetaDisposable()
-
+
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
-
+
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
-
+
self.maskNode = ASImageNode()
-
+ self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
+ self.avatarNode.isUserInteractionEnabled = false
+
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
-
+
super.init(layerBacked: false)
-
+
self.clipsToBounds = true
-
+
self.addSubnode(self.containerNode)
+ self.addSubnode(self.avatarNode)
}
-
+
deinit {
self.disposable.dispose()
}
-
+
func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
+ let currentItem = self.item
var currentBackgroundNode = self.backgroundNode
-
- return { item, params, neighbors in
- if currentBackgroundNode == nil {
- currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
- }
- currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
- currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
+ return { item, params, neighbors in
+ let canReuseCurrentNodes = currentItem?.avatarCornerRadius == item.avatarCornerRadius
+
+ if currentBackgroundNode == nil {
+ // WallpaperBackgroundNodeImpl.init touches `self.view` (portal source views), which asserts
+ // off the main thread. asyncLayout runs on a background queue, so create it on main.
+ if Thread.isMainThread {
+ currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
+ } else {
+ let context = item.context
+ let semaphore = DispatchSemaphore(value: 0)
+ var createdNode: WallpaperBackgroundNode?
+ Queue.mainQueue().async {
+ createdNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
+ semaphore.signal()
+ }
+ semaphore.wait()
+ currentBackgroundNode = createdNode
+ }
+ }
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
-
- let peerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(1))
- let otherPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(2))
+
+ // Use a fake group chat so incoming preview messages render author avatars
+ // (private-chat previews intentionally hide avatars; group/channel previews show them).
+ let chatPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(1))
+ let incomingAuthorId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(2))
+ let outgoingAuthorId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(3))
+
+ let chatPeer = TelegramGroup(id: chatPeerId, title: "Preview", photo: [], participantCount: 2, role: .member, membership: .Member, flags: TelegramGroupFlags(), defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
+ let outgoingAuthor = TelegramUser(id: outgoingAuthorId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
+
var items: [ListViewItem] = []
for messageItem in item.messageItems.reversed() {
var peers = EngineSimpleDictionary()
+ peers[chatPeerId] = chatPeer
var messages = EngineSimpleDictionary()
-
- let replyMessageId = EngineMessage.Id(peerId: peerId, namespace: 0, id: 3)
+
+ let replyMessageId = EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 3)
if let (author, text) = messageItem.reply {
- peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
- messages[replyMessageId] = EngineRawMessage(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: EngineSimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
+ let replyAuthor = TelegramUser(id: incomingAuthorId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
+ peers[incomingAuthorId] = replyAuthor
+ messages[replyMessageId] = EngineRawMessage(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: replyAuthor, text: text, attributes: [], media: [], peers: peers, associatedMessages: EngineSimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
}
-
- let message = EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
- items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil))
+
+ let author: EngineRawPeer
+ if messageItem.outgoing {
+ author = outgoingAuthor
+ } else {
+ author = TelegramUser(id: incomingAuthorId, accessHash: nil, firstName: "Winter", lastName: "Gram", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
+ }
+ peers[author.id] = author
+
+ let message = EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
+ items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: outgoingAuthor, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil))
}
-
+
var nodes: [ListViewItemNode] = []
- if let messageNodes = currentNodes {
+ if let messageNodes = currentNodes, canReuseCurrentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
@@ -181,12 +225,12 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
-
+
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
-
+
apply(ListViewItemApply(isOnScreen: true))
})
}
@@ -203,37 +247,64 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
}
nodes = messageNodes
}
-
+
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
for node in nodes {
contentSize.height += node.frame.size.height
}
insets = itemListNeighborsGroupedInsets(neighbors, params)
-
+
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
-
+
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
-
+
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
-
+
+ if !canReuseCurrentNodes {
+ currentNodes?.forEach { $0.removeFromSupernode() }
+ }
strongSelf.messageNodes = nodes
var topOffset: CGFloat = 4.0
- for node in nodes {
+ var avatarFrame: CGRect?
+ let displayedMessageItems = Array(item.messageItems.reversed())
+ for (nodeIndex, node) in nodes.enumerated() {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize)
+ if avatarFrame == nil, nodeIndex < displayedMessageItems.count, !displayedMessageItems[nodeIndex].outgoing {
+ let avatarSize: CGFloat = 40.0
+ avatarFrame = CGRect(
+ x: params.leftInset + 9.0,
+ y: topOffset + max(6.0, node.frame.height - avatarSize - 8.0),
+ width: avatarSize,
+ height: avatarSize
+ )
+ }
topOffset += node.frame.size.height
}
+ if let avatarPeer = item.avatarPeer, let avatarFrame {
+ strongSelf.avatarNode.isHidden = false
+ strongSelf.avatarNode.frame = avatarFrame
+ let avatarRadius = min(avatarFrame.width, avatarFrame.height) * CGFloat(max(0, min(50, item.avatarCornerRadius))) / 100.0
+ strongSelf.avatarNode.layer.cornerRadius = avatarRadius
+ strongSelf.avatarNode.layer.masksToBounds = true
+ strongSelf.avatarNode.setPeer(context: item.context, theme: item.componentTheme, peer: avatarPeer, synchronousLoad: false, displayDimensions: avatarFrame.size)
+ strongSelf.avatarNode.updateSize(size: avatarFrame.size)
+ } else {
+ strongSelf.avatarNode.isHidden = true
+ }
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
-
+ currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
+ currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
+
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
@@ -246,7 +317,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
-
+
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
@@ -270,11 +341,11 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
-
+
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
-
+
let backgroundFrame = 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)))
-
+
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
@@ -289,7 +360,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
displayMode = .aspectFill
}
}
-
+
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
@@ -301,11 +372,11 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
})
}
}
-
+
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
-
+
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift
index 965bca3666..cf51a3b67a 100644
--- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift
+++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift
@@ -24,6 +24,89 @@ import WallpaperGridScreen
import PeerNameColorItem
import DeviceModel
+private func avatarRadiusLabel(_ value: Int32) -> String {
+ switch value {
+ case 50: return "Round"
+ case 30: return "Squircle"
+ case 15: return "Rounded"
+ case 0: return "Square"
+ default: return "\(value)"
+ }
+}
+
+private func winterGramAssetDisplayTitle(_ rawName: String, prefixes: [String]) -> String? {
+ var name = rawName.components(separatedBy: "/").last ?? rawName
+ if let dotIndex = name.lastIndex(of: ".") {
+ name = String(name[.. String in
+ let lowercased = word.lowercased()
+ if lowercased == "wintergram" {
+ return "WinterGram"
+ }
+ if lowercased == "ios" {
+ return "iOS"
+ }
+ return lowercased.prefix(1).uppercased() + String(lowercased.dropFirst())
+ }
+
+ return words.isEmpty ? nil : words.joined(separator: " ")
+}
+
+private func winterGramBannerTitle(_ name: String) -> String {
+ if name == "WntGramBanner" {
+ return "WinterGram"
+ }
+ return winterGramAssetDisplayTitle(name, prefixes: [
+ "banner-",
+ "wntgram",
+ "WinterGram"
+ ]) ?? name
+}
+
+private func winterGramAvailableBannerNames(selectedBanner: String) -> [String] {
+ var names: [String] = ["WntGramBanner"]
+ if !selectedBanner.isEmpty && !names.contains(selectedBanner) {
+ names.append(selectedBanner)
+ }
+ return names.filter { UIImage(bundleImageName: $0) != nil }
+}
+
+private func winterGramTopBannerPreviewImage(theme: PresentationTheme, bannerName: String) -> UIImage {
+ let size = CGSize(width: 144.0, height: 36.0)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ return renderer.image { _ in
+ let bounds = CGRect(origin: .zero, size: size)
+ let plateRect = bounds.insetBy(dx: 1.0, dy: 1.0)
+ let platePath = UIBezierPath(roundedRect: plateRect, cornerRadius: 12.0)
+ theme.list.itemBlocksBackgroundColor.setFill()
+ platePath.fill()
+ theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.45).setStroke()
+ platePath.lineWidth = UIScreenPixel
+ platePath.stroke()
+
+ UIColor.black.setFill()
+ UIBezierPath(roundedRect: CGRect(x: floor((size.width - 44.0) / 2.0), y: 6.0, width: 44.0, height: 12.0), cornerRadius: 6.0).fill()
+
+ if let banner = UIImage(bundleImageName: bannerName) ?? UIImage(bundleImageName: "WntGramBanner") {
+ let bannerHeight: CGFloat = 13.0
+ let bannerWidth = min(size.width - 20.0, floor(bannerHeight * banner.size.width / max(1.0, banner.size.height)))
+ banner.draw(in: CGRect(x: floor((size.width - bannerWidth) / 2.0), y: 20.0, width: bannerWidth, height: bannerHeight))
+ }
+ }
+}
+
private final class ThemeSettingsControllerArguments {
let context: AccountContext
let selectTheme: (PresentationThemeReference) -> Void
@@ -44,7 +127,11 @@ private final class ThemeSettingsControllerArguments {
let editTheme: (PresentationCloudTheme) -> Void
let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void
let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
-
+ let updateWinterGramSettingsPreview: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void
+ let updateWinterGramSettings: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void
+ let toggleIconPackExpanded: () -> Void
+ let toggleBannerExpanded: () -> Void
+
init(
context: AccountContext,
selectTheme: @escaping (PresentationThemeReference) -> Void,
@@ -64,7 +151,11 @@ private final class ThemeSettingsControllerArguments {
selectAppIcon: @escaping (PresentationAppIcon) -> Void,
editTheme: @escaping (PresentationCloudTheme) -> Void,
themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void,
- colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
+ colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void,
+ updateWinterGramSettingsPreview: @escaping (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void,
+ updateWinterGramSettings: @escaping (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void,
+ toggleIconPackExpanded: @escaping () -> Void,
+ toggleBannerExpanded: @escaping () -> Void
) {
self.context = context
self.selectTheme = selectTheme
@@ -85,16 +176,22 @@ private final class ThemeSettingsControllerArguments {
self.editTheme = editTheme
self.themeContextAction = themeContextAction
self.colorContextAction = colorContextAction
+ self.updateWinterGramSettingsPreview = updateWinterGramSettingsPreview
+ self.updateWinterGramSettings = updateWinterGramSettings
+ self.toggleIconPackExpanded = toggleIconPackExpanded
+ self.toggleBannerExpanded = toggleBannerExpanded
}
}
private enum ThemeSettingsControllerSection: Int32 {
case chatPreview
+ case rounding
case nightMode
case message
case icon
case powerSaving
case other
+ case winterGram
}
public enum ThemeSettingsEntryTag: ItemListItemTag {
@@ -110,7 +207,7 @@ public enum ThemeSettingsEntryTag: ItemListItemTag {
case tapForNextMedia
case nightMode
case edit
-
+
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ThemeSettingsEntryTag, self == other {
return true
@@ -122,16 +219,20 @@ public enum ThemeSettingsEntryTag: ItemListItemTag {
private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case themeListHeader(PresentationTheme, String)
- case chatPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem])
+ case chatPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem], Int32, EnginePeer?, Bool)
case themes(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, Bool, [String: [StickerPackItem]], [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper])
case chatTheme(PresentationTheme, String)
case wallpaper(PresentationTheme, String)
case nameColor(PresentationTheme, String, String, PeerNameColors.Colors?, PeerNameColors.Colors?)
+ case winterGramRoundingHeader(PresentationTheme, String)
case autoNight(PresentationTheme, String, Bool, Bool)
case autoNightTheme(PresentationTheme, String, String)
case textSize(PresentationTheme, String, String)
case bubbleSettings(PresentationTheme, String, String)
case iconHeader(PresentationTheme, String)
+ case winterGramIconPack(PresentationTheme, WinterGramIconPack, Bool)
+ case winterGramBannerSelection(PresentationTheme, String, Bool)
+ case winterGramBannerPreview(PresentationTheme, String)
case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], Bool, String?)
case powerSaving
case stickersAndEmoji
@@ -139,69 +240,118 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case sendWithCmdEnter(PresentationTheme, String, Bool)
case showNextMediaOnTap(PresentationTheme, String, Bool)
case showNextMediaOnTapInfo(PresentationTheme, String)
-
+ case winterGramShowSeconds(PresentationTheme, String, Bool)
+ case winterGramHeader(PresentationTheme, String)
+ case winterGramMaterialDesign(PresentationTheme, String, Bool)
+ case winterGramSingleCornerRadius(PresentationTheme, String, Bool)
+ case winterGramAvatarRadius(PresentationTheme, String, Int32, Signal?)
+ case winterGramBubbleRadius(PresentationTheme, String, Int32)
+ case winterGramLiquidGlass(PresentationTheme, String, Bool)
+ case winterGramLiquidGlassVibrancy(PresentationTheme, String, Bool)
+ case winterGramLiquidGlassChatList(PresentationTheme, String, Bool)
+ case winterGramLiquidGlassNavBars(PresentationTheme, String, Bool)
+ case winterGramLiquidGlassTabBar(PresentationTheme, String, Bool)
+ case winterGramLiquidGlassBubbles(PresentationTheme, String, Bool)
+
var section: ItemListSectionId {
switch self {
case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper, .nameColor:
return ThemeSettingsControllerSection.chatPreview.rawValue
+ case .winterGramRoundingHeader, .winterGramShowSeconds, .winterGramAvatarRadius, .winterGramBubbleRadius:
+ return ThemeSettingsControllerSection.rounding.rawValue
case .autoNight, .autoNightTheme:
return ThemeSettingsControllerSection.nightMode.rawValue
case .textSize, .bubbleSettings:
return ThemeSettingsControllerSection.message.rawValue
- case .iconHeader, .iconItem:
+ case .iconHeader, .winterGramIconPack, .winterGramBannerSelection, .winterGramBannerPreview, .iconItem:
return ThemeSettingsControllerSection.icon.rawValue
case .powerSaving, .stickersAndEmoji:
return ThemeSettingsControllerSection.message.rawValue
case .otherHeader, .sendWithCmdEnter, .showNextMediaOnTap, .showNextMediaOnTapInfo:
return ThemeSettingsControllerSection.other.rawValue
+ case .winterGramHeader, .winterGramMaterialDesign, .winterGramSingleCornerRadius, .winterGramLiquidGlass, .winterGramLiquidGlassVibrancy, .winterGramLiquidGlassChatList, .winterGramLiquidGlassNavBars, .winterGramLiquidGlassTabBar, .winterGramLiquidGlassBubbles:
+ return ThemeSettingsControllerSection.winterGram.rawValue
}
}
-
+
var stableId: Int32 {
switch self {
case .themeListHeader:
return 0
case .chatPreview:
- return 1
- case .themes:
- return 2
- case .chatTheme:
- return 3
- case .wallpaper:
- return 4
- case .nameColor:
- return 5
- case .autoNight:
- return 6
- case .autoNightTheme:
- return 7
- case .textSize:
- return 8
- case .bubbleSettings:
- return 9
- case .powerSaving:
return 10
+ case .themes:
+ return 40
+ case .chatTheme:
+ return 50
+ case .wallpaper:
+ return 60
+ case .nameColor:
+ return 70
+ // Rounding sliders sit right after Personal Colors, before Night Mode.
+ case .winterGramRoundingHeader:
+ return 72
+ case .winterGramShowSeconds:
+ return 73
+ case .winterGramAvatarRadius:
+ return 74
+ case .winterGramBubbleRadius:
+ return 75
+ case .autoNight:
+ return 80
+ case .autoNightTheme:
+ return 90
+ case .textSize:
+ return 100
+ case .bubbleSettings:
+ return 110
+ case .powerSaving:
+ return 120
case .stickersAndEmoji:
- return 11
+ return 130
case .iconHeader:
- return 12
+ return 140
+ case .winterGramBannerSelection:
+ return 145
+ case .winterGramBannerPreview:
+ return 146
+ case .winterGramIconPack:
+ return 147
case .iconItem:
- return 13
+ return 150
case .otherHeader:
- return 14
+ return 160
case .sendWithCmdEnter:
- return 15
+ return 170
case .showNextMediaOnTap:
- return 16
+ return 180
case .showNextMediaOnTapInfo:
- return 17
+ return 190
+ case .winterGramHeader:
+ return 200
+ case .winterGramMaterialDesign:
+ return 210
+ case .winterGramSingleCornerRadius:
+ return 220
+ case .winterGramLiquidGlass:
+ return 230
+ case .winterGramLiquidGlassVibrancy:
+ return 240
+ case .winterGramLiquidGlassChatList:
+ return 250
+ case .winterGramLiquidGlassNavBars:
+ return 260
+ case .winterGramLiquidGlassTabBar:
+ return 270
+ case .winterGramLiquidGlassBubbles:
+ return 280
}
}
-
+
static func ==(lhs: ThemeSettingsControllerEntry, rhs: ThemeSettingsControllerEntry) -> Bool {
switch lhs {
- case let .chatPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems):
- if case let .chatPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems {
+ case let .chatPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems, lhsAvatarCornerRadius, lhsAvatarPeer, lhsShowSeconds):
+ if case let .chatPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems, rhsAvatarCornerRadius, rhsAvatarPeer, rhsShowSeconds) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems, lhsAvatarCornerRadius == rhsAvatarCornerRadius, lhsAvatarPeer == rhsAvatarPeer, lhsShowSeconds == rhsShowSeconds {
return true
} else {
return false
@@ -230,6 +380,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .winterGramRoundingHeader(lhsTheme, lhsText):
+ if case let .winterGramRoundingHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
case let .autoNight(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .autoNight(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
@@ -266,6 +422,24 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .winterGramIconPack(lhsTheme, lhsValue, lhsExpanded):
+ if case let .winterGramIconPack(rhsTheme, rhsValue, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsExpanded == rhsExpanded {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramBannerSelection(lhsTheme, lhsValue, lhsExpanded):
+ if case let .winterGramBannerSelection(rhsTheme, rhsValue, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsExpanded == rhsExpanded {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramBannerPreview(lhsTheme, lhsValue):
+ if case let .winterGramBannerPreview(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
case let .iconItem(lhsTheme, lhsStrings, lhsIcons, lhsIsPremium, lhsValue):
if case let .iconItem(rhsTheme, rhsStrings, rhsIcons, rhsIsPremium, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsIcons == rhsIcons, lhsIsPremium == rhsIsPremium, lhsValue == rhsValue {
return true
@@ -308,18 +482,90 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .winterGramShowSeconds(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramShowSeconds(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramHeader(lhsTheme, lhsText):
+ if case let .winterGramHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramMaterialDesign(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramMaterialDesign(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramSingleCornerRadius(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramSingleCornerRadius(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramAvatarRadius(lhsTheme, lhsText, lhsValue, _):
+ if case let .winterGramAvatarRadius(rhsTheme, rhsText, rhsValue, _) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramBubbleRadius(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramBubbleRadius(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlass(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlass(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlassVibrancy(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlassVibrancy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlassChatList(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlassChatList(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlassNavBars(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlassNavBars(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlassTabBar(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlassTabBar(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
+ case let .winterGramLiquidGlassBubbles(lhsTheme, lhsText, lhsValue):
+ if case let .winterGramLiquidGlassBubbles(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
}
}
-
+
static func <(lhs: ThemeSettingsControllerEntry, rhs: ThemeSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
-
+
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ThemeSettingsControllerArguments
switch self {
- case let .chatPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items):
- return ThemeSettingsChatPreviewItem(context: arguments.context, systemStyle: .glass, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items)
+ case let .chatPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items, avatarCornerRadius, avatarPeer, _):
+ return ThemeSettingsChatPreviewItem(context: arguments.context, systemStyle: .glass, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items, avatarCornerRadius: avatarCornerRadius, avatarPeer: avatarPeer)
case let .themes(theme, strings, chatThemes, currentTheme, nightMode, animatedEmojiStickers, themeSpecificAccentColors, themeSpecificChatWallpapers):
return ThemeCarouselThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: chatThemes, hasNoTheme: false, animatedEmojiStickers: animatedEmojiStickers, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, nightMode: nightMode, currentTheme: currentTheme, updatedTheme: { theme in
if let theme {
@@ -344,12 +590,14 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
if let profileColor {
colors.append(profileColor)
}
-
+
let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors)
-
+
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: "", labelStyle: .image(image: colorImage, size: colorImage.size), sectionId: self.section, style: .blocks, action: {
arguments.openNameColorSettings()
})
+ case let .winterGramRoundingHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .autoNight(_, title, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleNightTheme(value)
@@ -370,6 +618,32 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .iconHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .winterGramIconPack(_, value, expanded):
+ let options: [ItemListExpandableSelectionItem.Option] = [
+ ItemListExpandableSelectionItem.Option(id: WinterGramIconPack.wintergram.rawValue, title: wntOption("WinterGram", presentationData.strings), isSelected: value == .wintergram),
+ ItemListExpandableSelectionItem.Option(id: WinterGramIconPack.telegram.rawValue, title: wntOption("Telegram", presentationData.strings), isSelected: value == .telegram)
+ ]
+ return ItemListExpandableSelectionItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.WinterGram_IconPack, options: options, isExpanded: expanded, sectionId: self.section, style: .blocks, updated: { option in
+ if let rawValue = option.id.base as? Int32, let pack = WinterGramIconPack(rawValue: rawValue) {
+ arguments.updateWinterGramSettings { var s = $0; s.iconPack = pack; return s }
+ }
+ }, toggleExpanded: {
+ arguments.toggleIconPackExpanded()
+ })
+ case let .winterGramBannerSelection(_, selectedBanner, expanded):
+ let options = winterGramAvailableBannerNames(selectedBanner: selectedBanner).map { bannerName in
+ ItemListExpandableSelectionItem.Option(id: bannerName, title: winterGramBannerTitle(bannerName), isSelected: selectedBanner == bannerName)
+ }
+ return ItemListExpandableSelectionItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.WinterGram_TopBanner, options: options, isExpanded: expanded, sectionId: self.section, style: .blocks, updated: { option in
+ if let bannerName = option.id.base as? String {
+ arguments.updateWinterGramSettings { var s = $0; s.topBannerName = bannerName; s.topBannerStyle = .solid; return s }
+ }
+ }, toggleExpanded: {
+ arguments.toggleBannerExpanded()
+ })
+ case let .winterGramBannerPreview(theme, selectedBanner):
+ let previewImage = winterGramTopBannerPreviewImage(theme: theme, bannerName: selectedBanner)
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: "Preview", label: "", labelStyle: .image(image: previewImage, size: previewImage.size), sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
case let .iconItem(theme, strings, icons, isPremium, value):
return ThemeSettingsAppIconItem(theme: theme, strings: strings, systemStyle: .glass, sectionId: self.section, icons: icons, isPremium: isPremium, currentIconName: value, updated: { icon in
arguments.selectAppIcon(icon)
@@ -394,19 +668,74 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
}, tag: ThemeSettingsEntryTag.tapForNextMedia)
case let .showNextMediaOnTapInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
+ case let .winterGramShowSeconds(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettingsPreview { var s = $0; s.showMessageSeconds = value; return s }
+ arguments.updateWinterGramSettings { var s = $0; s.showMessageSeconds = value; return s }
+ })
+ case let .winterGramHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .winterGramMaterialDesign(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.materialDesign = value; return s }
+ })
+ case let .winterGramSingleCornerRadius(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.singleCornerRadius = value; return s }
+ })
+ case let .winterGramAvatarRadius(_, title, value, avatarSignal):
+ return WinterGramRadiusItem(presentationData: presentationData, title: title, value: value, minValue: 0, maxValue: 50, previewKind: .avatar, avatarSignal: avatarSignal, displayValue: { wntOption(avatarRadiusLabel($0), presentationData.strings) }, sectionId: self.section, valueChanged: { newValue in
+ arguments.updateWinterGramSettingsPreview { var s = $0; s.avatarCornerRadius = newValue; return s }
+ }, updated: { newValue in
+ arguments.updateWinterGramSettings { var s = $0; s.avatarCornerRadius = newValue; return s }
+ })
+ case let .winterGramBubbleRadius(_, title, value):
+ return WinterGramRadiusItem(presentationData: presentationData, title: title, value: value, minValue: 0, maxValue: 20, previewKind: .bubble, displayValue: { "\($0)" }, sectionId: self.section, valueChanged: { newValue in
+ arguments.updateWinterGramSettingsPreview { var s = $0; s.messageBubbleRadius = newValue; return s }
+ }, updated: { newValue in
+ arguments.updateWinterGramSettings { var s = $0; s.messageBubbleRadius = newValue; return s }
+ })
+ case let .winterGramLiquidGlass(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.enabled = value; return s }
+ })
+ case let .winterGramLiquidGlassVibrancy(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.vibrancy = value; return s }
+ })
+ case let .winterGramLiquidGlassChatList(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.applyToChatList = value; return s }
+ })
+ case let .winterGramLiquidGlassNavBars(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.applyToNavigationBars = value; return s }
+ })
+ case let .winterGramLiquidGlassTabBar(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.applyToTabBar = value; return s }
+ })
+ case let .winterGramLiquidGlassBubbles(_, title, value):
+ return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateWinterGramSettings { var s = $0; s.liquidGlass.applyToBubbles = value; return s }
+ })
}
}
}
private func themeSettingsControllerEntries(
+ context: AccountContext,
presentationData: PresentationData,
presentationThemeSettings: PresentationThemeSettings,
chatSettings: ChatSettings,
mediaSettings: MediaDisplaySettings,
+ winterGramSettings: WinterGramSettings,
themeReference: PresentationThemeReference,
availableThemes: [PresentationThemeReference],
availableAppIcons: [PresentationAppIcon],
currentAppIconName: String?,
+ iconPackExpanded: Bool,
+ bannerExpanded: Bool,
isPremium: Bool,
chatThemes: [PresentationThemeReference],
animatedEmojiStickers: [String: [StickerPackItem]],
@@ -414,13 +743,14 @@ private func themeSettingsControllerEntries(
nameColors: PeerNameColors
) -> [ThemeSettingsControllerEntry] {
var entries: [ThemeSettingsControllerEntry] = []
-
+
let strings = presentationData.strings
let title = presentationData.autoNightModeTriggered ? strings.Appearance_ColorThemeNight.uppercased() : strings.Appearance_ColorTheme.uppercased()
entries.append(.themeListHeader(presentationData.theme, title))
-
+
let nameColor: PeerColor
let profileColor: PeerNameColor?
+ let previewPhoto: [TelegramMediaImageRepresentation]
var authorName = presentationData.strings.Appearance_PreviewReplyAuthor
if let accountPeer {
nameColor = accountPeer.nameColor ?? .preset(.blue)
@@ -428,17 +758,21 @@ private func themeSettingsControllerEntries(
authorName = accountPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
}
profileColor = accountPeer.effectiveProfileColor
+ previewPhoto = accountPeer.profileImageRepresentations
} else {
nameColor = .preset(.blue)
profileColor = nil
+ previewPhoto = []
}
-
- entries.append(.chatPreview(presentationData.theme, presentationData.chatWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (authorName, presentationData.strings.Appearance_PreviewReplyText), text: presentationData.strings.Appearance_PreviewIncomingText, nameColor: nameColor, backgroundEmojiId: accountPeer?.backgroundEmojiId), ChatPreviewMessageItem(outgoing: true, reply: nil, text: presentationData.strings.Appearance_PreviewOutgoingText, nameColor: .preset(.blue), backgroundEmojiId: nil)]))
-
+
+ var previewChatBubbleCorners = presentationData.chatBubbleCorners
+ previewChatBubbleCorners.mainRadius = CGFloat(max(0, min(20, winterGramSettings.messageBubbleRadius)))
+ entries.append(.chatPreview(presentationData.theme, presentationData.chatWallpaper, presentationData.chatFontSize, previewChatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (authorName, presentationData.strings.Appearance_PreviewReplyText), text: presentationData.strings.Appearance_PreviewIncomingText, nameColor: nameColor, photo: previewPhoto, backgroundEmojiId: accountPeer?.backgroundEmojiId), ChatPreviewMessageItem(outgoing: true, reply: nil, text: presentationData.strings.Appearance_PreviewOutgoingText, nameColor: .preset(.blue), backgroundEmojiId: nil)], winterGramSettings.avatarCornerRadius, accountPeer, winterGramSettings.showMessageSeconds))
+
entries.append(.themes(presentationData.theme, presentationData.strings, chatThemes, themeReference, presentationThemeSettings.automaticThemeSwitchSetting.force || presentationData.autoNightModeTriggered, animatedEmojiStickers, presentationThemeSettings.themeSpecificAccentColors, presentationThemeSettings.themeSpecificChatWallpapers))
entries.append(.chatTheme(presentationData.theme, strings.Settings_ChatThemes))
entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground))
-
+
let colors: PeerNameColors.Colors
switch nameColor {
case let .preset(nameColor):
@@ -448,7 +782,14 @@ private func themeSettingsControllerEntries(
}
let profileColors = profileColor.flatMap { nameColors.getProfile($0, dark: presentationData.theme.overallDarkAppearance, subject: .palette) }
entries.append(.nameColor(presentationData.theme, presentationData.strings.Settings_YourColor, accountPeer?.compactDisplayTitle ?? "", colors, profileColors))
-
+
+ // Rounding sliders, placed right after Personal Colors. The single theme preview at the top
+ // already reflects both avatar and message rounding, so there is no separate rounding preview.
+ entries.append(.winterGramRoundingHeader(presentationData.theme, "WINTERGRAM"))
+ entries.append(.winterGramShowSeconds(presentationData.theme, strings.WinterGram_ShowMessageSeconds, winterGramSettings.showMessageSeconds))
+ entries.append(.winterGramAvatarRadius(presentationData.theme, strings.WinterGram_AvatarShape, winterGramSettings.avatarCornerRadius, nil))
+ entries.append(.winterGramBubbleRadius(presentationData.theme, strings.WinterGram_BubbleRadius, winterGramSettings.messageBubbleRadius))
+
entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force))
let autoNightMode: String
switch presentationThemeSettings.automaticThemeSwitchSetting.trigger {
@@ -466,7 +807,7 @@ private func themeSettingsControllerEntries(
autoNightMode = strings.AutoNightTheme_Automatic
}
entries.append(.autoNightTheme(presentationData.theme, strings.Appearance_AutoNightTheme, autoNightMode))
-
+
let textSizeValue: String
if presentationThemeSettings.useSystemFont {
textSizeValue = strings.Appearance_TextSize_Automatic
@@ -481,24 +822,38 @@ private func themeSettingsControllerEntries(
entries.append(.bubbleSettings(presentationData.theme, strings.Appearance_BubbleCornersSetting, ""))
entries.append(.powerSaving)
entries.append(.stickersAndEmoji)
-
+
if !availableAppIcons.isEmpty {
entries.append(.iconHeader(presentationData.theme, strings.Appearance_AppIcon.uppercased()))
- entries.append(.iconItem(presentationData.theme, presentationData.strings, availableAppIcons, isPremium, currentAppIconName))
+ entries.append(.winterGramBannerSelection(presentationData.theme, winterGramSettings.topBannerName, bannerExpanded))
+ entries.append(.winterGramBannerPreview(presentationData.theme, winterGramSettings.topBannerName))
+ entries.append(.winterGramIconPack(presentationData.theme, winterGramSettings.iconPack, iconPackExpanded))
+ let filteredAppIcons = availableAppIcons.filter { icon in
+ let isWinterGramIcon = icon.name.hasPrefix("WinterGram") || icon.name.hasPrefix("icon-app-")
+ switch winterGramSettings.iconPack {
+ case .wintergram:
+ return isWinterGramIcon
+ case .telegram:
+ return !isWinterGramIcon
+ default:
+ return true
+ }
+ }
+ entries.append(.iconItem(presentationData.theme, presentationData.strings, filteredAppIcons.isEmpty ? availableAppIcons : filteredAppIcons, isPremium, currentAppIconName))
}
-
+
entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased()))
if DeviceModel.current.isIpad {
entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter))
}
entries.append(.showNextMediaOnTap(presentationData.theme, strings.Appearance_ShowNextMediaOnTap, mediaSettings.showNextMediaOnTap))
entries.append(.showNextMediaOnTapInfo(presentationData.theme, strings.Appearance_ShowNextMediaOnTapInfo))
-
+
return entries
}
public protocol ThemeSettingsController {
-
+
}
private final class ThemeSettingsControllerImpl: ItemListController, ThemeSettingsController {
@@ -515,13 +870,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)?
var getNavigationControllerImpl: (() -> NavigationController?)?
var presentCrossfadeControllerImpl: ((Bool) -> Void)?
-
+
var selectThemeImpl: ((PresentationThemeReference) -> Void)?
var selectAccentColorImpl: ((PresentationThemeAccentColor?) -> Void)?
var openAccentColorPickerImpl: ((PresentationThemeReference, Bool) -> Void)?
-
+
let _ = context.engine.themes.wallpapers().start()
-
+
let currentAppIcon: PresentationAppIcon?
var appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons()
if let alternateIconName = context.sharedContext.applicationBindings.getAlternateIconName() {
@@ -529,26 +884,26 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
currentAppIcon = appIcons.filter { $0.isDefault }.first
}
-
+
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if premiumConfiguration.isPremiumDisabled || context.account.testingEnvironment {
- appIcons = appIcons.filter { !$0.isPremium }
+ appIcons = appIcons.filter { !$0.isPremium }
}
-
+
let availableAppIcons: Signal<[PresentationAppIcon], NoError> = .single(appIcons)
let currentAppIconName = ValuePromise()
currentAppIconName.set(currentAppIcon?.name ?? "Blue")
-
+
let cloudThemes = Promise<[TelegramTheme]>()
let updatedCloudThemes = context.engine.themes.themes(accountManager: context.sharedContext.accountManager)
cloudThemes.set(updatedCloudThemes)
-
+
let removedThemeIndexesPromise = Promise>(Set())
let removedThemeIndexes = Atomic>(value: Set())
-
+
let archivedPacks = Promise<[ArchivedStickerPackItem]?>()
archivedPacks.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init)))
-
+
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|> map { animatedEmoji -> [String: [StickerPackItem]] in
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
@@ -568,7 +923,14 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
return animatedEmojiStickers
}
-
+
+ let winterGramPreviewSettingsValue = Atomic(value: nil)
+ let winterGramPreviewSettingsPromise = ValuePromise(nil, ignoreRepeated: false)
+ let iconPackExpandedValue = Atomic(value: true)
+ let iconPackExpandedPromise = ValuePromise(true, ignoreRepeated: true)
+ let bannerExpandedValue = Atomic(value: false)
+ let bannerExpandedPromise = ValuePromise(false, ignoreRepeated: true)
+
let arguments = ThemeSettingsControllerArguments(context: context, selectTheme: { theme in
selectThemeImpl?(theme)
}, openThemeSettings: {
@@ -635,6 +997,11 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
pushControllerImpl?(controller)
} else {
currentAppIconName.set(icon.name)
+ let _ = updateWinterGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
+ var updated = current
+ updated.appIcon = icon.name
+ return updated
+ }).startStandalone()
context.sharedContext.applicationBindings.requestSetAlternateIconName(icon.isDefault ? nil : icon.name, { _ in
})
}
@@ -703,7 +1070,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let strings = presentationData.strings
let themeController = ThemePreviewController(context: context, previewTheme: theme, source: .settings(reference, wallpaper, true))
var items: [ContextMenuItem] = []
-
+
if case let .cloud(theme) = reference {
if theme.theme.isCreator {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_EditTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in
@@ -718,7 +1085,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
})
})
-
+
c?.dismiss(completion: {
pushControllerImpl?(controller)
})
@@ -728,7 +1095,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, preview: false) else {
return
}
-
+
let resolvedWallpaper: Signal
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(engine: context.engine, network: context.account.network, slug: file.slug, settings: file.settings)
@@ -738,7 +1105,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
resolvedWallpaper = .single(theme.chat.defaultWallpaper)
}
-
+
let _ = (resolvedWallpaper
|> deliverOnMainQueue).start(next: { wallpaper in
let controller = ThemeAccentColorController(context: context, mode: .edit(settings: nil, theme: theme, wallpaper: wallpaper, generalThemeReference: reference.generalThemeReference, defaultThemeReference: nil, create: true, completion: { result, settings in
@@ -766,7 +1133,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
return controllers
})
}))
-
+
c?.dismiss(completion: {
pushControllerImpl?(controller)
})
@@ -796,7 +1163,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
updated.insert(theme.theme.id)
return updated
})))
-
+
if isCurrent, let currentThemeIndex = themes.firstIndex(where: { $0.id == theme.theme.id }) {
if let settings = theme.theme.settings?.first {
if settings.baseTheme == .night {
@@ -816,7 +1183,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
selectThemeImpl?(newTheme)
}
}
-
+
let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: theme.theme).start()
})
}))
@@ -837,7 +1204,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
})
})))
}
-
+
let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController, nil)
})
@@ -866,7 +1233,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
generalThemeReference = reference
}
-
+
let effectiveWallpaper: TelegramWallpaper
let effectiveThemeReference: PresentationThemeReference
if let accentColor = accentColor, case let .theme(themeReference) = accentColor {
@@ -874,7 +1241,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
effectiveThemeReference = reference
}
-
+
if let wallpaper = wallpaper {
effectiveWallpaper = wallpaper
} else {
@@ -893,7 +1260,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
effectiveWallpaper = theme?.chat.defaultWallpaper ?? .builtin(WallpaperSettings())
}
-
+
let wallpaperSignal: Signal
if case let .file(file) = effectiveWallpaper, file.id == 0 {
wallpaperSignal = cachedWallpaper(engine: context.engine, network: context.account.network, slug: file.slug, settings: file.settings)
@@ -903,7 +1270,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
wallpaperSignal = .single(effectiveWallpaper)
}
-
+
return wallpaperSignal
|> mapToSignal { wallpaper in
return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.sharedContext.accountManager.mediaBox)
@@ -946,7 +1313,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let strings = presentationData.strings
let themeController = ThemePreviewController(context: context, previewTheme: theme, source: .settings(effectiveThemeReference, wallpaper, true))
var items: [ContextMenuItem] = []
-
+
if let accentColor = accentColor {
if case let .accentColor(color) = accentColor, color.baseColor != .custom {
} else if case let .theme(theme) = accentColor, case let .cloud(cloudTheme) = theme {
@@ -963,7 +1330,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
})
})
-
+
c?.dismiss(completion: {
pushControllerImpl?(controller)
})
@@ -973,7 +1340,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: effectiveThemeReference, preview: false) else {
return
}
-
+
let resolvedWallpaper: Signal
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(engine: context.engine, network: context.account.network, slug: file.slug, settings: file.settings)
@@ -983,7 +1350,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
resolvedWallpaper = .single(theme.chat.defaultWallpaper)
}
-
+
let _ = (resolvedWallpaper
|> deliverOnMainQueue).start(next: { wallpaper in
var hasSettings = false
@@ -1016,7 +1383,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
return controllers
})
}))
-
+
c?.dismiss(completion: {
pushControllerImpl?(controller)
})
@@ -1047,7 +1414,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
updated.insert(cloudTheme.theme.id)
return updated
})))
-
+
if isCurrent, let settings = cloudTheme.theme.settings?.first {
let colorThemes = themes.filter { theme in
if let _ = theme.settings {
@@ -1056,7 +1423,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
return false
}
}
-
+
if let currentThemeIndex = colorThemes.firstIndex(where: { $0.id == cloudTheme.theme.id }) {
let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil })
if let previousThemeIndex = previousThemeIndex {
@@ -1071,7 +1438,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
}
}
-
+
let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: cloudTheme.theme).start()
})
}))
@@ -1089,32 +1456,63 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController, nil)
})
+ }, updateWinterGramSettingsPreview: { f in
+ let updatedSettings = f(winterGramPreviewSettingsValue.with { $0 } ?? currentWinterGramSettings)
+ let _ = winterGramPreviewSettingsValue.swap(updatedSettings)
+ setCurrentWinterGramSettings(updatedSettings)
+ winterGramPreviewSettingsPromise.set(updatedSettings)
+ }, updateWinterGramSettings: { f in
+ let _ = (updateWinterGramSettingsInteractively(accountManager: context.sharedContext.accountManager, f)
+ |> deliverOnMainQueue).startStandalone(next: { _ in
+ let _ = winterGramPreviewSettingsValue.swap(nil)
+ winterGramPreviewSettingsPromise.set(nil)
+ })
+ }, toggleIconPackExpanded: {
+ let nextValue = !iconPackExpandedValue.with { $0 }
+ let _ = iconPackExpandedValue.swap(nextValue)
+ iconPackExpandedPromise.set(nextValue)
+ }, toggleBannerExpanded: {
+ let nextValue = !bannerExpandedValue.with { $0 }
+ let _ = bannerExpandedValue.swap(nextValue)
+ bannerExpandedPromise.set(nextValue)
})
- let signal = combineLatest(
+ let sharedDataAndWinterGramPreview = combineLatest(
queue: .mainQueue(),
- context.sharedContext.presentationData,
context.sharedContext.accountManager.sharedData(keys: [
ApplicationSpecificSharedDataKeys.presentationThemeSettings,
ApplicationSpecificSharedDataKeys.chatSettings,
ApplicationSpecificSharedDataKeys.mediaDisplaySettings,
+ ApplicationSpecificSharedDataKeys.winterGramSettings,
SharedDataKeys.chatThemes
]),
+ winterGramPreviewSettingsPromise.get()
+ )
+
+ let signal = combineLatest(
+ queue: .mainQueue(),
+ context.sharedContext.presentationData,
+ sharedDataAndWinterGramPreview,
cloudThemes.get(),
availableAppIcons,
currentAppIconName.get(),
+ iconPackExpandedPromise.get(),
+ bannerExpandedPromise.get(),
removedThemeIndexesPromise.get(),
animatedEmojiStickers,
context.account.postbox.peerView(id: context.account.peerId),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
- |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ |> map { presentationData, sharedDataAndWinterGramPreview, cloudThemes, availableAppIcons, currentAppIconName, iconPackExpanded, bannerExpanded, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ let (sharedData, winterGramPreviewSettings) = sharedDataAndWinterGramPreview
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let chatSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) ?? ChatSettings.defaultSettings
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings
-
+ let storedWinterGramSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.winterGramSettings]?.get(WinterGramSettings.self) ?? WinterGramSettings.defaultSettings
+ let winterGramSettings = winterGramPreviewSettings ?? storedWinterGramSettings
+
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
-
+
let themeReference: PresentationThemeReference
if presentationData.autoNightModeTriggered {
if let _ = settings.theme.emoticon {
@@ -1125,7 +1523,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
themeReference = settings.theme
}
-
+
var defaultThemes: [PresentationThemeReference] = []
if presentationData.autoNightModeTriggered {
defaultThemes.append(contentsOf: [.builtin(.nightAccent), .builtin(.night)])
@@ -1137,24 +1535,24 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
.builtin(.night)
])
}
-
+
let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }.filter { !removedThemeIndexes.contains($0.index) }
-
+
var availableThemes = defaultThemes
if defaultThemes.first(where: { $0.index == themeReference.index }) == nil && cloudThemes.first(where: { $0.index == themeReference.index }) == nil {
availableThemes.append(themeReference)
}
availableThemes.append(contentsOf: cloudThemes)
-
+
var chatThemes = cloudThemes.filter { $0.emoticon != nil }
chatThemes.insert(.builtin(.dayClassic), at: 0)
-
+
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
- let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, chatSettings: chatSettings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
-
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(context: context, presentationData: presentationData, presentationThemeSettings: settings, chatSettings: chatSettings, mediaSettings: mediaSettings, winterGramSettings: winterGramSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, iconPackExpanded: iconPackExpanded, bannerExpanded: bannerExpanded, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
+
return (controllerState, (listState, arguments))
}
-
+
let controller = ThemeSettingsControllerImpl(context: context, state: signal)
controller.alwaysSynchronous = true
pushControllerImpl = { [weak controller] c in
@@ -1180,12 +1578,12 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
var bottomOffset: CGFloat?
var leftOffset: CGFloat?
var themeItemNode: ThemeCarouselThemeItemNode?
-
+
var view: UIView?
if #available(iOS 11.0, *) {
view = controller.navigationController?.view
}
-
+
let controllerFrame = controller.view.convert(controller.view.bounds, to: controller.navigationController?.view)
if controllerFrame.minX > 0.0 {
leftOffset = controllerFrame.minX
@@ -1193,7 +1591,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
if controllerFrame.minY > 100.0 {
view = nil
}
-
+
controller.forEachItemNode { node in
if let itemNode = node as? ItemListItemNode {
if let itemTag = itemNode.tag {
@@ -1208,7 +1606,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
}
}
-
+
if let navigationBar = controller.navigationBar {
if let offset = topOffset {
topOffset = max(offset, navigationBar.frame.maxY)
@@ -1216,20 +1614,20 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
topOffset = navigationBar.frame.maxY
}
}
-
+
if view != nil {
themeItemNode?.prepareCrossfadeTransition()
}
-
+
let sectionInset = max(16.0, floor((controller.displayNode.frame.width - 674.0) / 2.0))
-
+
let crossfadeController = ThemeSettingsCrossfadeController(view: view, topOffset: topOffset, bottomOffset: bottomOffset, leftOffset: leftOffset, sideInset: sectionInset)
crossfadeController.didAppear = { [weak themeItemNode] in
if view != nil {
themeItemNode?.animateCrossfadeTransition()
}
}
-
+
context.sharedContext.presentGlobalController(crossfadeController, nil)
}
}
@@ -1237,9 +1635,9 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else {
return
}
-
+
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered
-
+
let resolvedWallpaper: Signal
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(engine: context.engine, network: context.account.network, slug: file.slug, settings: file.settings)
@@ -1249,13 +1647,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
resolvedWallpaper = .single(nil)
}
-
+
var cloudTheme: TelegramTheme?
if case let .cloud(theme) = theme {
cloudTheme = theme.theme
}
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: cloudTheme).start()
-
+
let currentTheme = context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeReference) in
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings)?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
if autoNightModeTriggered {
@@ -1264,7 +1662,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
return settings.theme
}
}
-
+
let _ = (combineLatest(resolvedWallpaper, currentTheme)
|> map { resolvedWallpaper, currentTheme -> Bool in
var updatedTheme = theme
@@ -1274,7 +1672,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} else {
currentThemeBaseIndex = currentTheme.index
}
-
+
var baseThemeIndex: Int64?
var updatedThemeBaseIndex: Int64?
if case let .cloud(info) = theme {
@@ -1303,7 +1701,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
return current.withUpdatedTheme(updatedTheme).withUpdatedAutomaticThemeSwitchSetting(updatedAutomaticThemeSwitchSetting)
}).start()
-
+
return currentThemeBaseIndex != updatedThemeBaseIndex
} |> deliverOnMainQueue).start(next: { crossfadeAccentColors in
presentCrossfadeControllerImpl?((cloudTheme == nil || cloudTheme?.settings != nil) && !crossfadeAccentColors)
@@ -1325,13 +1723,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let _ = context.engine.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start()
return .single(wallpaper)
-
+
} else {
return .single(nil)
}
}
}
-
+
let _ = (wallpaperSignal
|> deliverOnMainQueue).start(next: { presetWallpaper in
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
@@ -1340,33 +1738,33 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
if autoNightModeTriggered {
currentTheme = current.automaticThemeSwitchSetting.theme
}
-
+
let generalThemeReference: PresentationThemeReference
if case let .cloud(theme) = currentTheme, let settings = theme.theme.settings?.first {
generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme))
} else {
generalThemeReference = currentTheme
}
-
+
currentTheme = generalThemeReference
var updatedTheme = current.theme
var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting
-
+
if autoNightModeTriggered {
updatedAutomaticThemeSwitchSetting.theme = generalThemeReference
} else {
updatedTheme = generalThemeReference
}
-
+
guard let _ = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.color, wallpaper: presetWallpaper, baseColor: accentColor?.baseColor) else {
return current
}
-
+
let themePreferredBaseTheme = current.themePreferredBaseTheme
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
var themeSpecificAccentColors = current.themeSpecificAccentColors
themeSpecificAccentColors[generalThemeReference.index] = accentColor?.withUpdatedWallpaper(presetWallpaper)
-
+
if case .builtin = generalThemeReference {
let index = coloredThemeIndex(reference: currentTheme, accentColor: accentColor)
if let wallpaper = current.themeSpecificChatWallpapers[index] {
@@ -1377,14 +1775,14 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
themeSpecificChatWallpapers[index] = presetWallpaper
}
}
-
+
return PresentationThemeSettings(theme: updatedTheme, themePreferredBaseTheme: themePreferredBaseTheme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: updatedAutomaticThemeSwitchSetting, largeEmoji: current.largeEmoji, reduceMotion: current.reduceMotion)
}).start()
-
+
presentCrossfadeControllerImpl?(true)
})
}
-
+
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
@@ -1398,22 +1796,22 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
}
}
-
+
return controller
}
public final class ThemeSettingsCrossfadeController: ViewController {
private var snapshotView: UIView?
-
+
private var topSnapshotView: UIView?
private var bottomSnapshotView: UIView?
private var sideSnapshotView: UIView?
-
+
private var leftSnapshotView: UIView?
private var rightSnapshotView: UIView?
-
+
var didAppear: (() -> Void)?
-
+
public init(view: UIView? = nil, topOffset: CGFloat? = nil, bottomOffset: CGFloat? = nil, leftOffset: CGFloat? = nil, sideInset: CGFloat = 0.0) {
if let view = view {
if let leftOffset = leftOffset {
@@ -1421,61 +1819,61 @@ public final class ThemeSettingsCrossfadeController: ViewController {
let clipView = UIView()
clipView.clipsToBounds = true
clipView.addSubview(view)
-
+
view.clipsToBounds = true
view.contentMode = .topLeft
-
+
if let topOffset = topOffset, let bottomOffset = bottomOffset {
var frame = view.frame
frame.origin.y = topOffset
frame.size.width = leftOffset + sideInset
frame.size.height = bottomOffset - topOffset
clipView.frame = frame
-
+
frame = view.frame
frame.origin.y = -topOffset
frame.size.width = leftOffset + sideInset
frame.size.height = bottomOffset
view.frame = frame
}
-
+
self.sideSnapshotView = clipView
}
}
-
+
if sideInset > 0.0 {
if let view = view.snapshotView(afterScreenUpdates: false), leftOffset == nil {
let clipView = UIView()
clipView.clipsToBounds = true
clipView.addSubview(view)
-
+
view.clipsToBounds = true
view.contentMode = .topLeft
-
+
if let topOffset = topOffset, let bottomOffset = bottomOffset {
var frame = view.frame
frame.origin.y = topOffset
frame.size.width = sideInset
frame.size.height = bottomOffset - topOffset
clipView.frame = frame
-
+
frame = view.frame
frame.origin.y = -topOffset
frame.size.width = sideInset
frame.size.height = bottomOffset
view.frame = frame
}
-
+
self.leftSnapshotView = clipView
}
if let view = view.snapshotView(afterScreenUpdates: false) {
let clipView = UIView()
clipView.clipsToBounds = true
clipView.addSubview(view)
-
+
view.clipsToBounds = true
view.contentMode = .topRight
-
+
if let topOffset = topOffset, let bottomOffset = bottomOffset {
var frame = view.frame
frame.origin.x = frame.width - sideInset
@@ -1483,18 +1881,18 @@ public final class ThemeSettingsCrossfadeController: ViewController {
frame.size.width = sideInset
frame.size.height = bottomOffset - topOffset
clipView.frame = frame
-
+
frame = view.frame
frame.origin.y = -topOffset
frame.size.width = sideInset
frame.size.height = bottomOffset
view.frame = frame
}
-
+
self.rightSnapshotView = clipView
}
}
-
+
if let view = view.snapshotView(afterScreenUpdates: false) {
view.clipsToBounds = true
view.contentMode = .top
@@ -1505,7 +1903,7 @@ public final class ThemeSettingsCrossfadeController: ViewController {
}
self.topSnapshotView = view
}
-
+
if let view = view.snapshotView(afterScreenUpdates: false) {
view.clipsToBounds = true
view.contentMode = .bottom
@@ -1521,17 +1919,17 @@ public final class ThemeSettingsCrossfadeController: ViewController {
self.snapshotView = UIScreen.main.snapshotView(afterScreenUpdates: false)
}
super.init(navigationBarPresentationData: nil)
-
+
self.statusBar.statusBarStyle = .Ignore
}
-
+
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
override public func loadDisplayNode() {
self.displayNode = ViewControllerTracingNode()
-
+
self.displayNode.backgroundColor = nil
self.displayNode.isOpaque = false
self.displayNode.isUserInteractionEnabled = false
@@ -1554,14 +1952,14 @@ public final class ThemeSettingsCrossfadeController: ViewController {
self.displayNode.view.addSubview(rightSnapshotView)
}
}
-
+
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
-
+
self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
})
-
+
self.didAppear?()
}
}
@@ -1569,16 +1967,16 @@ public final class ThemeSettingsCrossfadeController: ViewController {
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceNode: ASDisplayNode?
-
+
let navigationController: NavigationController? = nil
-
+
let passthroughTouches: Bool = false
-
+
init(controller: ViewController, sourceNode: ASDisplayNode?) {
self.controller = controller
self.sourceNode = sourceNode
}
-
+
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
@@ -1589,7 +1987,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
})
}
-
+
func animatedIn() {
}
}
diff --git a/submodules/SettingsUI/Sources/WinterGramBannerItem.swift b/submodules/SettingsUI/Sources/WinterGramBannerItem.swift
new file mode 100644
index 0000000000..755d47714d
--- /dev/null
+++ b/submodules/SettingsUI/Sources/WinterGramBannerItem.swift
@@ -0,0 +1,167 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import SwiftSignalKit
+import TelegramPresentationData
+import ItemListUI
+import PresentationDataUtils
+
+// A hero banner shown at the very top of the WinterGram settings menu, just under the navigation
+// bar / Dynamic Island: a snowflake app-tile, the WinterGram name and a short tagline.
+private func winterGramBannerIcon(size: CGSize) -> UIImage {
+ let renderer = UIGraphicsImageRenderer(size: size)
+ return renderer.image { rendererContext in
+ let context = rendererContext.cgContext
+ let bounds = CGRect(origin: .zero, size: size)
+ context.saveGState()
+ UIBezierPath(roundedRect: bounds, cornerRadius: size.height * 0.225).addClip()
+ let colors = [UIColor(rgb: 0x5CC0F5).cgColor, UIColor(rgb: 0x2D7FD6).cgColor] as CFArray
+ if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0.0, 1.0]) {
+ context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: [])
+ }
+ context.restoreGState()
+ if let glyph = UIImage(systemName: "snowflake", withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * 0.56, weight: .semibold))?.withTintColor(.white, renderingMode: .alwaysOriginal) {
+ let glyphSize = glyph.size
+ glyph.draw(in: CGRect(x: floor((size.width - glyphSize.width) / 2.0), y: floor((size.height - glyphSize.height) / 2.0), width: glyphSize.width, height: glyphSize.height))
+ }
+ }
+}
+
+// Renders the user's current app icon as a rounded-rect tile for the banner.
+private func winterGramBannerRoundedIcon(image: UIImage, size: CGSize) -> UIImage {
+ let renderer = UIGraphicsImageRenderer(size: size)
+ return renderer.image { _ in
+ let bounds = CGRect(origin: .zero, size: size)
+ UIBezierPath(roundedRect: bounds, cornerRadius: size.height * 0.225).addClip()
+ image.draw(in: bounds)
+ }
+}
+
+class WinterGramBannerItem: ListViewItem, ItemListItem {
+ let theme: PresentationTheme
+ let title: String
+ let subtitle: String
+ let iconImage: UIImage?
+ let sectionId: ItemListSectionId
+ let isAlwaysPlain: Bool = true
+
+ init(theme: PresentationTheme, title: String, subtitle: String, iconImage: UIImage? = nil, sectionId: ItemListSectionId) {
+ self.theme = theme
+ self.title = title
+ self.subtitle = subtitle
+ self.iconImage = iconImage
+ self.sectionId = sectionId
+ }
+
+ func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
+ async {
+ Queue.mainQueue().async {
+ let node = WinterGramBannerItemNode()
+ let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ node.contentSize = layout.contentSize
+ node.insets = layout.insets
+ completion(node, {
+ return (nil, { _ in apply() })
+ })
+ }
+ }
+ }
+
+ 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) {
+ Queue.mainQueue().async {
+ if let nodeValue = node() as? WinterGramBannerItemNode {
+ let makeLayout = nodeValue.asyncLayout()
+ async {
+ let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ Queue.mainQueue().async {
+ completion(layout, { _ in
+ apply()
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+class WinterGramBannerItemNode: ListViewItemNode {
+ private let backgroundNode: ASDisplayNode
+ private let iconNode: ASImageNode
+ private let titleNode: ImmediateTextNode
+ private let subtitleNode: ImmediateTextNode
+
+ private var item: WinterGramBannerItem?
+
+ init() {
+ self.backgroundNode = ASDisplayNode()
+ self.backgroundNode.isLayerBacked = true
+ self.iconNode = ASImageNode()
+ self.iconNode.displaysAsynchronously = false
+ self.iconNode.isUserInteractionEnabled = false
+ self.titleNode = ImmediateTextNode()
+ self.titleNode.displaysAsynchronously = false
+ self.titleNode.textAlignment = .center
+ self.subtitleNode = ImmediateTextNode()
+ self.subtitleNode.displaysAsynchronously = false
+ self.subtitleNode.textAlignment = .center
+ self.subtitleNode.maximumNumberOfLines = 2
+
+ super.init(layerBacked: false)
+
+ self.insertSubnode(self.backgroundNode, at: 0)
+ self.addSubnode(self.iconNode)
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.subtitleNode)
+ }
+
+ func asyncLayout() -> (_ item: WinterGramBannerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
+ return { item, params, _ in
+ let hasSubtitle = !item.subtitle.isEmpty
+ let iconSize = CGSize(width: hasSubtitle ? 96.0 : 80.0, height: hasSubtitle ? 96.0 : 80.0)
+ let topInset: CGFloat = hasSubtitle ? 14.0 : 10.0
+ let iconTitleSpacing: CGFloat = hasSubtitle ? 10.0 : 8.0
+ let titleSubtitleSpacing: CGFloat = 4.0
+
+ let titleFont = Font.semibold(hasSubtitle ? 26.0 : 22.0)
+ let subtitleFont = Font.regular(14.0)
+ let constrainedWidth = params.width - params.leftInset - params.rightInset - 40.0
+
+ let contentHeight: CGFloat = hasSubtitle ? 196.0 : 142.0
+ let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: UIEdgeInsets())
+
+ return (layout, { [weak self] in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.item = item
+ let width = params.width
+
+ // No grey backplate behind the banner — it sits flat on the grouped background.
+ strongSelf.backgroundNode.backgroundColor = .clear
+ strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: contentHeight))
+
+ if let iconImage = item.iconImage {
+ strongSelf.iconNode.image = winterGramBannerRoundedIcon(image: iconImage, size: iconSize)
+ } else {
+ strongSelf.iconNode.image = winterGramBannerIcon(size: iconSize)
+ }
+
+ strongSelf.titleNode.attributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
+ let titleSize = strongSelf.titleNode.updateLayout(CGSize(width: constrainedWidth, height: 30.0))
+
+ var subtitleSize = CGSize()
+ if hasSubtitle {
+ strongSelf.subtitleNode.attributedText = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.theme.list.itemSecondaryTextColor)
+ subtitleSize = strongSelf.subtitleNode.updateLayout(CGSize(width: constrainedWidth, height: 40.0))
+ } else {
+ strongSelf.subtitleNode.attributedText = nil
+ }
+
+ strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((width - iconSize.width) / 2.0), y: topInset), size: iconSize)
+ strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: topInset + iconSize.height + iconTitleSpacing), size: titleSize)
+ strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: topInset + iconSize.height + iconTitleSpacing + titleSize.height + titleSubtitleSpacing), size: subtitleSize)
+ })
+ }
+ }
+}
diff --git a/submodules/SettingsUI/Sources/WinterGramMainSettingsController.swift b/submodules/SettingsUI/Sources/WinterGramMainSettingsController.swift
new file mode 100644
index 0000000000..2388216ac6
--- /dev/null
+++ b/submodules/SettingsUI/Sources/WinterGramMainSettingsController.swift
@@ -0,0 +1,304 @@
+import Foundation
+import UIKit
+import Display
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import TelegramUIPreferences
+import ItemListUI
+import PresentationDataUtils
+import AccountContext
+
+private final class WinterGramMainSettingsArguments {
+ let openCategory: (WinterGramSettingsSection) -> Void
+ let openUrl: (String) -> Void
+ let updateSettings: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void
+ let appVersionString: String
+
+ init(
+ openCategory: @escaping (WinterGramSettingsSection) -> Void,
+ openUrl: @escaping (String) -> Void,
+ updateSettings: @escaping (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void,
+ appVersionString: String
+ ) {
+ self.openCategory = openCategory
+ self.openUrl = openUrl
+ self.updateSettings = updateSettings
+ self.appVersionString = appVersionString
+ }
+}
+
+private enum WinterGramMainSettingsSection: Int32 {
+ case banner
+ case categories
+ case information
+ case links
+}
+
+// A link row shown in the "Links" section: SF Symbol, label, accent-coloured value and target URL.
+private struct WinterGramLink {
+ let symbol: String
+ let imageName: String?
+ let title: String
+ let value: String
+ let url: String
+
+ init(symbol: String, imageName: String? = nil, title: String, value: String, url: String) {
+ self.symbol = symbol
+ self.imageName = imageName
+ self.title = title
+ self.value = value
+ self.url = url
+ }
+}
+
+private let winterGramLinks: [WinterGramLink] = [
+ WinterGramLink(symbol: "paperplane.fill", title: "Channel", value: "@wntgram", url: "https://t.me/wntgram"),
+ WinterGramLink(symbol: "sparkles", title: "Beta", value: "@wntbeta", url: "https://t.me/wntbeta"),
+ WinterGramLink(symbol: "bubble.left.and.bubble.right.fill", title: "Chat", value: "@wntForum", url: "https://t.me/wntForum"),
+ WinterGramLink(symbol: "puzzlepiece.extension.fill", title: "Plugins", value: "@wntPlugins", url: "https://t.me/wntPlugins"),
+ WinterGramLink(symbol: "link", imageName: "Item List/Icons/GitHub", title: "GitHub", value: "reekeer/WinterGram", url: "https://github.com/reekeer/WinterGram")
+]
+
+private enum WinterGramMainSettingsEntry: ItemListNodeEntry {
+ case banner
+ case categoriesHeader
+ case ayugram
+ case features
+ case other
+ case spoofing
+ case hiddenArchive
+ case informationHeader
+ case infoAbout
+ case infoUseDefaultBranding(Bool)
+ case infoVersion
+ case infoFooter
+ case linksHeader
+ case link(Int, WinterGramLink)
+ case linksFooter
+
+ var section: ItemListSectionId {
+ switch self {
+ case .banner:
+ return WinterGramMainSettingsSection.banner.rawValue
+ case .linksHeader, .link, .linksFooter:
+ return WinterGramMainSettingsSection.links.rawValue
+ case .informationHeader, .infoAbout, .infoUseDefaultBranding, .infoVersion, .infoFooter:
+ return WinterGramMainSettingsSection.information.rawValue
+ default:
+ return WinterGramMainSettingsSection.categories.rawValue
+ }
+ }
+
+ var stableId: Int32 {
+ switch self {
+ case .banner: return -1
+ case .categoriesHeader: return 10
+ case .ayugram: return 11
+ case .features: return 12
+ case .other: return 14
+ case .spoofing: return 15
+ case .hiddenArchive: return 16
+ case .informationHeader: return 20
+ case .infoAbout: return 21
+ case .infoUseDefaultBranding: return 22
+ case .infoVersion: return 23
+ case .infoFooter: return 25
+ case .linksHeader: return 30
+ case let .link(index, _): return 31 + Int32(index)
+ case .linksFooter: return 100
+ }
+ }
+
+ static func ==(lhs: WinterGramMainSettingsEntry, rhs: WinterGramMainSettingsEntry) -> Bool {
+ return lhs.stableId == rhs.stableId
+ }
+
+ static func <(lhs: WinterGramMainSettingsEntry, rhs: WinterGramMainSettingsEntry) -> Bool {
+ return lhs.stableId < rhs.stableId
+ }
+
+ func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
+ let arguments = arguments as! WinterGramMainSettingsArguments
+ let accent = presentationData.theme.list.itemAccentColor
+ let lang = presentationData.strings
+ let category: WinterGramSettingsSection
+ let title: String
+ let iconName: String
+ let iconColor: UIColor
+ switch self {
+ case .banner:
+ return WinterGramBannerItem(theme: presentationData.theme, title: "WinterGram", subtitle: "", iconImage: UIImage(bundleImageName: "WinterGramDark"), sectionId: self.section)
+ case .categoriesHeader:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.WinterGram_Categories, sectionId: self.section)
+ case .informationHeader:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_INFORMATION, sectionId: self.section)
+ case .infoAbout:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_WinterGramWntIsAPrivacyFocusedMessagingClientForIPhoneANativePortOfTheAyuGramExperienceItAddsGhostModeSavedDeletedMessagesAndEditHistoryAHiddenArchiveLocalPremiumAdRemovalDeepCustomizationAndLiquidGlass), sectionId: self.section)
+ case let .infoUseDefaultBranding(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_UseDefaultTelegramBranding, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.useDefaultBranding = value; return s }
+ })
+ case .infoVersion:
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_Version, label: arguments.appVersionString, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
+ case .infoFooter:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_InfoFooter), sectionId: self.section)
+ case .linksHeader:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.WinterGram_Links, sectionId: self.section)
+ case .linksFooter:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_LinksFooter), sectionId: self.section)
+ case let .link(_, link):
+ return ItemListDisclosureItem(
+ presentationData: presentationData,
+ icon: winterGramCategoryIcon(symbolName: link.symbol, imageName: link.imageName, backgroundColor: UIColor(rgb: 0x8E8E93)),
+ title: wntOption(link.title, presentationData.strings),
+ label: link.value,
+ labelStyle: .coloredText(accent),
+ sectionId: self.section,
+ style: .blocks,
+ disclosureStyle: .none,
+ action: {
+ arguments.openUrl(link.url)
+ }
+ )
+ case .ayugram:
+ category = .ayugram; title = "Core"; iconName = "shield.fill"; iconColor = UIColor(rgb: 0x5856D6)
+ case .features:
+ category = .antiFeatures; title = "Features"; iconName = "sparkles"; iconColor = UIColor(rgb: 0xFF9500)
+ case .other:
+ category = .other; title = "Other"; iconName = "ellipsis.circle"; iconColor = UIColor(rgb: 0x8E8E93)
+ case .spoofing:
+ category = .spoofing; title = "Spoofing"; iconName = "theatermasks"; iconColor = UIColor(rgb: 0xFF3B30)
+ case .hiddenArchive:
+ category = .stash; title = "Hidden Archive"; iconName = "tray.full.fill"; iconColor = UIColor(rgb: 0x34C759)
+ }
+ return ItemListDisclosureItem(
+ presentationData: presentationData,
+ icon: winterGramCategoryIcon(iconName, iconColor),
+ title: wntOption(title, presentationData.strings),
+ label: "",
+ sectionId: self.section,
+ style: .blocks,
+ disclosureStyle: .arrow,
+ action: {
+ arguments.openCategory(category)
+ }
+ )
+ }
+}
+
+/// Renders a rounded-rect backplate filled with `backgroundColor` and a white SF Symbol centred on
+/// top — a clean settings tile look.
+private func winterGramCategoryIcon(_ symbolName: String, _ backgroundColor: UIColor) -> UIImage? {
+ return winterGramCategoryIcon(symbolName: symbolName, imageName: nil, backgroundColor: backgroundColor)
+}
+
+private func winterGramCategoryIcon(symbolName: String, imageName: String?, backgroundColor: UIColor) -> UIImage? {
+ let size = CGSize(width: 44.0, height: 44.0)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ return renderer.image { _ in
+ let bounds = CGRect(origin: .zero, size: size)
+ // Smaller colored backplate with a transparent margin so the white icon dominates.
+ let backplateInset: CGFloat = 5.0
+ let backplateRect = bounds.insetBy(dx: backplateInset, dy: backplateInset)
+ let backplate = UIBezierPath(roundedRect: backplateRect, cornerRadius: 9.0)
+ backgroundColor.setFill()
+ backplate.fill()
+ let icon: UIImage?
+ if let imageName = imageName {
+ icon = UIImage(bundleImageName: imageName)?.withRenderingMode(.alwaysTemplate)
+ } else {
+ icon = UIImage(systemName: symbolName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 28.0, weight: .medium))?.withRenderingMode(.alwaysTemplate)
+ }
+ guard let symbol = icon?.withTintColor(.white, renderingMode: .alwaysOriginal) else {
+ return
+ }
+ let maxIconSide: CGFloat = 30.0
+ let symbolScale = min(maxIconSide / max(symbol.size.width, 1.0), maxIconSide / max(symbol.size.height, 1.0), 1.0)
+ let symbolSize = CGSize(width: symbol.size.width * symbolScale, height: symbol.size.height * symbolScale)
+ symbol.draw(in: CGRect(
+ x: floor((size.width - symbolSize.width) / 2.0),
+ y: floor((size.height - symbolSize.height) / 2.0),
+ width: symbolSize.width,
+ height: symbolSize.height
+ ))
+ }
+}
+
+private func winterGramMainSettingsEntries(settings: WinterGramSettings) -> [WinterGramMainSettingsEntry] {
+ var entries: [WinterGramMainSettingsEntry] = [
+ .banner,
+ .categoriesHeader,
+ .ayugram,
+ .features,
+ .other,
+ .spoofing,
+ .hiddenArchive,
+ .linksHeader
+ ]
+ for (index, link) in winterGramLinks.enumerated() {
+ entries.append(.link(index, link))
+ }
+ entries.append(.linksFooter)
+ return entries
+}
+
+public func winterGramMainSettingsController(context: AccountContext) -> ViewController {
+ var pushControllerImpl: ((ViewController) -> Void)?
+ var getNavigationControllerImpl: (() -> NavigationController?)?
+
+ let accountManager = context.sharedContext.accountManager
+ let updateSettings: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void = { f in
+ let _ = updateWinterGramSettingsInteractively(accountManager: accountManager, f).startStandalone()
+ }
+
+ let appVersionString: String = {
+ let version = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? ""
+ let build = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? ""
+ return build.isEmpty ? version : "\(version) (\(build))"
+ }()
+
+ let arguments = WinterGramMainSettingsArguments(
+ openCategory: { category in
+ pushControllerImpl?(winterGramSettingsController(context: context, category: category))
+ },
+ openUrl: { url in
+ // Open WinterGram channels in-app (resolve t.me links) rather than bouncing to the browser.
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: getNavigationControllerImpl?(), dismissInput: {})
+ },
+ updateSettings: updateSettings,
+ appVersionString: appVersionString
+ )
+
+ let signal = combineLatest(queue: .mainQueue(),
+ context.sharedContext.presentationData,
+ winterGramSettings(accountManager: context.sharedContext.accountManager)
+ )
+ |> deliverOnMainQueue
+ |> map { presentationData, settings -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ let controllerState = ItemListControllerState(
+ presentationData: ItemListPresentationData(presentationData),
+ title: .text("WinterGram"),
+ leftNavigationButton: nil,
+ rightNavigationButton: nil,
+ backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
+ )
+ let listState = ItemListNodeState(
+ presentationData: ItemListPresentationData(presentationData),
+ entries: winterGramMainSettingsEntries(settings: settings),
+ style: .blocks,
+ animateChanges: true
+ )
+ return (controllerState, (listState, arguments))
+ }
+
+ let controller = ItemListController(context: context, state: signal)
+ pushControllerImpl = { [weak controller] c in
+ controller?.push(c)
+ }
+ getNavigationControllerImpl = { [weak controller] in
+ return controller?.navigationController as? NavigationController
+ }
+ return controller
+}
diff --git a/submodules/SettingsUI/Sources/WinterGramOnlineTrackerController.swift b/submodules/SettingsUI/Sources/WinterGramOnlineTrackerController.swift
new file mode 100644
index 0000000000..bd9178437e
--- /dev/null
+++ b/submodules/SettingsUI/Sources/WinterGramOnlineTrackerController.swift
@@ -0,0 +1,134 @@
+import Foundation
+import UIKit
+import Display
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import TelegramUIPreferences
+import ItemListUI
+import PresentationDataUtils
+import AccountContext
+
+private final class WinterGramOnlineTrackerArguments {
+ let openHistory: (EnginePeer.Id) -> Void
+ let clearAll: () -> Void
+ init(openHistory: @escaping (EnginePeer.Id) -> Void, clearAll: @escaping () -> Void) {
+ self.openHistory = openHistory
+ self.clearAll = clearAll
+ }
+}
+
+private enum WinterGramOnlineTrackerSection: Int32 {
+ case peers
+ case actions
+}
+
+private enum WinterGramOnlineTrackerEntry: ItemListNodeEntry {
+ case peer(index: Int, peerId: EnginePeer.Id, name: String, summary: String)
+ case clear
+ case footer
+
+ var section: ItemListSectionId {
+ switch self {
+ case .peer: return WinterGramOnlineTrackerSection.peers.rawValue
+ case .clear, .footer: return WinterGramOnlineTrackerSection.actions.rawValue
+ }
+ }
+
+ var stableId: Int32 {
+ switch self {
+ case let .peer(index, _, _, _): return Int32(index)
+ case .clear: return 100000
+ case .footer: return 100001
+ }
+ }
+
+ static func <(lhs: WinterGramOnlineTrackerEntry, rhs: WinterGramOnlineTrackerEntry) -> Bool {
+ return lhs.stableId < rhs.stableId
+ }
+
+ func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
+ let arguments = arguments as! WinterGramOnlineTrackerArguments
+ switch self {
+ case let .peer(_, peerId, name, summary):
+ return ItemListDisclosureItem(presentationData: presentationData, title: name, label: summary, sectionId: self.section, style: .blocks, action: {
+ arguments.openHistory(peerId)
+ })
+ case .clear:
+ return ItemListActionItem(presentationData: presentationData, title: "Clear Tracking Log", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
+ arguments.clearAll()
+ })
+ case .footer:
+ return ItemListTextItem(presentationData: presentationData, text: .plain("Online transitions are recorded locally while a chat is open. Tap a name to see its history."), sectionId: self.section)
+ }
+ }
+}
+
+private func formatEntry(_ entry: WinterGramPresenceEntry) -> String {
+ let date = Date(timeIntervalSince1970: TimeInterval(entry.timestamp))
+ let formatter = DateFormatter()
+ formatter.dateFormat = "dd.MM HH:mm"
+ return "\(formatter.string(from: date)) — \(entry.isOnline ? "online" : "offline")"
+}
+
+public func winterGramOnlineTrackerController(context: AccountContext) -> ViewController {
+ var presentControllerImpl: ((ViewController, Any?) -> Void)?
+ let refreshPromise = ValuePromise(true, ignoreRepeated: false)
+
+ let arguments = WinterGramOnlineTrackerArguments(
+ openHistory: { peerId in
+ let entries = winterGramPresenceLog(peerId: peerId.toInt64())
+ let text = entries.isEmpty ? "No records yet." : entries.reversed().prefix(40).map(formatEntry).joined(separator: "\n")
+ let controller = textAlertController(context: context, title: nil, text: text, actions: [
+ TextAlertAction(type: .defaultAction, title: context.sharedContext.currentPresentationData.with { $0 }.strings.Common_OK, action: {})
+ ])
+ presentControllerImpl?(controller, nil)
+ },
+ clearAll: {
+ winterGramClearPresenceLog()
+ refreshPromise.set(true)
+ }
+ )
+
+ let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, refreshPromise.get())
+ |> mapToSignal { presentationData, _ -> Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError> in
+ let peerIds = winterGramTrackedPeerIds()
+ let enginePeerIds = peerIds.map { EnginePeer.Id($0) }
+ return context.engine.data.get(EngineDataMap(enginePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
+ |> map { peerMap -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ var entries: [WinterGramOnlineTrackerEntry] = []
+ var index = 0
+ for pid in enginePeerIds {
+ let name: String
+ if let maybePeer = peerMap[pid], let peer = maybePeer {
+ name = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
+ } else {
+ name = "\(pid.toInt64())"
+ }
+ let log = winterGramPresenceLog(peerId: pid.toInt64())
+ let summary: String
+ if let last = log.last {
+ summary = last.isOnline ? "online" : "offline"
+ } else {
+ summary = ""
+ }
+ entries.append(.peer(index: index, peerId: pid, name: name, summary: summary))
+ index += 1
+ }
+ if !entries.isEmpty {
+ entries.append(.clear)
+ }
+ entries.append(.footer)
+
+ let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Online Tracker"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks)
+ return (controllerState, (listState, arguments))
+ }
+ }
+
+ let controller = ItemListController(context: context, state: signal)
+ presentControllerImpl = { [weak controller] c, a in
+ controller?.present(c, in: .window(.root), with: a)
+ }
+ return controller
+}
diff --git a/submodules/SettingsUI/Sources/WinterGramRadiusItem.swift b/submodules/SettingsUI/Sources/WinterGramRadiusItem.swift
new file mode 100644
index 0000000000..42f86f7ece
--- /dev/null
+++ b/submodules/SettingsUI/Sources/WinterGramRadiusItem.swift
@@ -0,0 +1,332 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import SwiftSignalKit
+import TelegramPresentationData
+import LegacyComponents
+import ItemListUI
+import PresentationDataUtils
+
+enum WinterGramRadiusPreviewKind {
+ case avatar
+ case bubble
+}
+
+// Draws a small live preview that reflects the chosen corner radius: a sample avatar
+// (the user's actual avatar when available, otherwise a gradient rounded square with a person glyph)
+// or a sample chat bubble.
+private func winterGramRadiusPreviewImage(kind: WinterGramRadiusPreviewKind, value: Int32, minValue: Int32, maxValue: Int32, size: CGSize, theme: PresentationTheme, avatarImage: UIImage? = nil) -> UIImage {
+ let renderer = UIGraphicsImageRenderer(size: size)
+ return renderer.image { rendererContext in
+ let context = rendererContext.cgContext
+ let bounds = CGRect(origin: .zero, size: size)
+ switch kind {
+ case .avatar:
+ let fraction = max(0.0, min(1.0, CGFloat(value - minValue) / CGFloat(max(1, maxValue - minValue))))
+ let cornerRadius = (size.height / 2.0) * fraction
+ context.saveGState()
+ UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
+ if let avatarImage = avatarImage {
+ avatarImage.draw(in: bounds)
+ } else {
+ let colors = [UIColor(rgb: 0x4FB3F0).cgColor, UIColor(rgb: 0x2D7FD6).cgColor] as CFArray
+ if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0.0, 1.0]) {
+ context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: [])
+ }
+ if let glyph = UIImage(systemName: "person.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * 0.46, weight: .semibold))?.withTintColor(.white, renderingMode: .alwaysOriginal) {
+ let glyphSize = glyph.size
+ glyph.draw(in: CGRect(x: floor((size.width - glyphSize.width) / 2.0), y: floor((size.height - glyphSize.height) / 2.0), width: glyphSize.width, height: glyphSize.height))
+ }
+ }
+ context.restoreGState()
+ case .bubble:
+ let bubbleHeight = size.height * 0.74
+ let bubbleRect = CGRect(x: 0.0, y: floor((size.height - bubbleHeight) / 2.0), width: size.width, height: bubbleHeight)
+ let cornerRadius = min(CGFloat(max(0, value)), bubbleHeight / 2.0)
+ theme.list.itemAccentColor.setFill()
+ UIBezierPath(roundedRect: bubbleRect, cornerRadius: cornerRadius).fill()
+ }
+ }
+}
+
+class WinterGramRadiusItem: ListViewItem, ItemListItem {
+ let presentationData: ItemListPresentationData
+ let title: String
+ let value: Int32
+ let minValue: Int32
+ let maxValue: Int32
+ let previewKind: WinterGramRadiusPreviewKind
+ let avatarSignal: Signal?
+ let displayValue: (Int32) -> String
+ let sectionId: ItemListSectionId
+ let valueChanged: ((Int32) -> Void)?
+ let updated: (Int32) -> Void
+
+ init(presentationData: ItemListPresentationData, title: String, value: Int32, minValue: Int32, maxValue: Int32, previewKind: WinterGramRadiusPreviewKind, avatarSignal: Signal? = nil, displayValue: @escaping (Int32) -> String, sectionId: ItemListSectionId, valueChanged: ((Int32) -> Void)? = nil, updated: @escaping (Int32) -> Void) {
+ self.presentationData = presentationData
+ self.title = title
+ self.value = value
+ self.minValue = minValue
+ self.maxValue = maxValue
+ self.previewKind = previewKind
+ self.avatarSignal = avatarSignal
+ self.displayValue = displayValue
+ self.sectionId = sectionId
+ self.valueChanged = valueChanged
+ self.updated = updated
+ }
+
+ func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
+ async {
+ let node = WinterGramRadiusItemNode()
+ let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ node.contentSize = layout.contentSize
+ node.insets = layout.insets
+ Queue.mainQueue().async {
+ completion(node, {
+ return (nil, { _ in apply() })
+ })
+ }
+ }
+ }
+
+ 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) {
+ Queue.mainQueue().async {
+ if let nodeValue = node() as? WinterGramRadiusItemNode {
+ let makeLayout = nodeValue.asyncLayout()
+ async {
+ let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
+ Queue.mainQueue().async {
+ completion(layout, { _ in
+ apply()
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+class WinterGramRadiusItemNode: ListViewItemNode {
+ private let backgroundNode: ASDisplayNode
+ private let topStripeNode: ASDisplayNode
+ private let bottomStripeNode: ASDisplayNode
+ private let maskNode: ASImageNode
+
+ private let titleNode: ImmediateTextNode
+ private let valueNode: ImmediateTextNode
+ private let previewNode: ASImageNode
+ private var sliderView: TGPhotoEditorSliderView?
+
+ private var item: WinterGramRadiusItem?
+ private var layoutParams: ListViewItemLayoutParams?
+ private var currentValue: Int32 = 0
+ private var currentAvatarImage: UIImage?
+ private var avatarDisposable: Disposable?
+
+ private let previewSize = CGSize(width: 48.0, height: 48.0)
+
+ init() {
+ self.backgroundNode = ASDisplayNode()
+ self.backgroundNode.isLayerBacked = true
+ self.topStripeNode = ASDisplayNode()
+ self.topStripeNode.isLayerBacked = true
+ self.bottomStripeNode = ASDisplayNode()
+ self.bottomStripeNode.isLayerBacked = true
+ self.maskNode = ASImageNode()
+
+ self.titleNode = ImmediateTextNode()
+ self.titleNode.displaysAsynchronously = false
+ self.valueNode = ImmediateTextNode()
+ self.valueNode.displaysAsynchronously = false
+ self.previewNode = ASImageNode()
+ self.previewNode.displaysAsynchronously = false
+ self.previewNode.isUserInteractionEnabled = false
+
+ super.init(layerBacked: false)
+
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.valueNode)
+ // WinterGram: the inline radius preview next to the slider is intentionally NOT shown — the
+ // rounding is previewed in the chat preview instead. The slider spans the full width.
+ }
+
+ deinit {
+ self.avatarDisposable?.dispose()
+ }
+
+ private func sliderFrame(params: ListViewItemLayoutParams) -> CGRect {
+ let sliderInsetLeft = params.leftInset + 16.0
+ let sliderInsetRight = params.width - params.rightInset - 16.0
+ return CGRect(origin: CGPoint(x: sliderInsetLeft, y: 42.0), size: CGSize(width: max(0.0, sliderInsetRight - sliderInsetLeft), height: 44.0))
+ }
+
+ private func applySliderTheme(_ sliderView: TGPhotoEditorSliderView, theme: PresentationTheme) {
+ sliderView.backgroundColor = .clear
+ sliderView.backColor = theme.list.itemSecondaryTextColor.withAlphaComponent(0.35)
+ sliderView.trackColor = theme.list.itemAccentColor
+ sliderView.knobImage = PresentationResourcesItemList.knobImage(theme)
+ }
+
+ override func didLoad() {
+ super.didLoad()
+
+ let sliderView = TGPhotoEditorSliderView()
+ sliderView.enablePanHandling = true
+ sliderView.trackCornerRadius = 2.0
+ sliderView.lineSize = 4.0
+ sliderView.disablesInteractiveTransitionGestureRecognizer = true
+ if let item = self.item, let params = self.layoutParams {
+ self.applySliderTheme(sliderView, theme: item.presentationData.theme)
+ sliderView.minimumValue = CGFloat(item.minValue)
+ sliderView.maximumValue = CGFloat(item.maxValue)
+ sliderView.startValue = CGFloat(item.minValue)
+ sliderView.value = CGFloat(item.value)
+ sliderView.frame = self.sliderFrame(params: params)
+ }
+ self.view.addSubview(sliderView)
+ sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
+ sliderView.interactionEnded = { [weak self] in
+ guard let self, let sliderView = self.sliderView else {
+ return
+ }
+ self.item?.updated(Int32(round(sliderView.value)))
+ }
+ self.sliderView = sliderView
+ }
+
+ func asyncLayout() -> (_ item: WinterGramRadiusItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
+ return { item, params, neighbors in
+ let contentSize = CGSize(width: params.width, height: 96.0)
+ let insets = itemListNeighborsGroupedInsets(neighbors, params)
+ let separatorHeight = UIScreenPixel
+ let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
+ let layoutSize = layout.size
+
+ let theme = item.presentationData.theme
+ let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
+ let valueFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
+
+ return (layout, { [weak self] in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.item = item
+ strongSelf.layoutParams = params
+ strongSelf.currentValue = item.value
+
+ strongSelf.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
+ strongSelf.topStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+ strongSelf.bottomStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
+
+ if strongSelf.backgroundNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
+ }
+ if strongSelf.topStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
+ }
+ if strongSelf.bottomStripeNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
+ }
+ if strongSelf.maskNode.supernode == nil {
+ strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
+ }
+
+ let hasCorners = itemListHasRoundedBlockLayout(params)
+ var hasTopCorners = false
+ var hasBottomCorners = false
+ switch neighbors.top {
+ case .sameSection(false):
+ strongSelf.topStripeNode.isHidden = true
+ default:
+ hasTopCorners = true
+ strongSelf.topStripeNode.isHidden = hasCorners
+ }
+ let bottomStripeInset: CGFloat
+ let bottomStripeOffset: CGFloat
+ switch neighbors.bottom {
+ case .sameSection(false):
+ bottomStripeInset = params.leftInset + 16.0
+ bottomStripeOffset = -separatorHeight
+ strongSelf.bottomStripeNode.isHidden = false
+ default:
+ bottomStripeInset = 0.0
+ bottomStripeOffset = 0.0
+ hasBottomCorners = true
+ strongSelf.bottomStripeNode.isHidden = hasCorners
+ }
+
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : 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)
+ strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
+ strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
+
+ strongSelf.titleNode.attributedText = NSAttributedString(string: item.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor)
+ let titleSize = strongSelf.titleNode.updateLayout(CGSize(width: params.width - params.leftInset - params.rightInset - 120.0, height: 30.0))
+ strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 13.0), size: titleSize)
+
+ strongSelf.valueNode.attributedText = NSAttributedString(string: item.displayValue(item.value), font: valueFont, textColor: theme.list.itemSecondaryTextColor)
+ let valueSize = strongSelf.valueNode.updateLayout(CGSize(width: 160.0, height: 30.0))
+ strongSelf.valueNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - valueSize.width, y: 14.0), size: valueSize)
+
+ strongSelf.previewNode.image = winterGramRadiusPreviewImage(kind: item.previewKind, value: item.value, minValue: item.minValue, maxValue: item.maxValue, size: strongSelf.previewSize, theme: theme, avatarImage: strongSelf.currentAvatarImage)
+ strongSelf.previewNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 40.0), size: strongSelf.previewSize)
+
+ if let sliderView = strongSelf.sliderView {
+ strongSelf.applySliderTheme(sliderView, theme: theme)
+ sliderView.minimumValue = CGFloat(item.minValue)
+ sliderView.maximumValue = CGFloat(item.maxValue)
+ sliderView.startValue = CGFloat(item.minValue)
+ if Int32(round(sliderView.value)) != item.value {
+ sliderView.value = CGFloat(item.value)
+ }
+ sliderView.frame = strongSelf.sliderFrame(params: params)
+ }
+
+ if let avatarSignal = item.avatarSignal {
+ strongSelf.avatarDisposable?.dispose()
+ strongSelf.avatarDisposable = (avatarSignal
+ |> deliverOnMainQueue).start(next: { [weak strongSelf] image in
+ guard let strongSelf = strongSelf else {
+ return
+ }
+ strongSelf.currentAvatarImage = image
+ if let item = strongSelf.item, let params = strongSelf.layoutParams {
+ strongSelf.previewNode.image = winterGramRadiusPreviewImage(kind: item.previewKind, value: strongSelf.currentValue, minValue: item.minValue, maxValue: item.maxValue, size: strongSelf.previewSize, theme: item.presentationData.theme, avatarImage: image)
+ strongSelf.previewNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 40.0), size: strongSelf.previewSize)
+ }
+ })
+ }
+ })
+ }
+ }
+
+ @objc private func sliderValueChanged() {
+ guard let sliderView = self.sliderView, let item = self.item, let params = self.layoutParams else {
+ return
+ }
+ let newValue = Int32(round(sliderView.value))
+ guard newValue != self.currentValue else {
+ return
+ }
+ self.currentValue = newValue
+ item.valueChanged?(newValue)
+ let theme = item.presentationData.theme
+ let valueFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
+ self.valueNode.attributedText = NSAttributedString(string: item.displayValue(newValue), font: valueFont, textColor: theme.list.itemSecondaryTextColor)
+ let valueSize = self.valueNode.updateLayout(CGSize(width: 160.0, height: 30.0))
+ self.valueNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - valueSize.width, y: 14.0), size: valueSize)
+ self.previewNode.image = winterGramRadiusPreviewImage(kind: item.previewKind, value: newValue, minValue: item.minValue, maxValue: item.maxValue, size: self.previewSize, theme: theme, avatarImage: self.currentAvatarImage)
+ }
+
+ override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
+ self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
+ }
+
+ override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
+ self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
+ }
+}
diff --git a/submodules/SettingsUI/Sources/WinterGramSettingsController.swift b/submodules/SettingsUI/Sources/WinterGramSettingsController.swift
index 97a2721faf..863ae1f440 100644
--- a/submodules/SettingsUI/Sources/WinterGramSettingsController.swift
+++ b/submodules/SettingsUI/Sources/WinterGramSettingsController.swift
@@ -6,68 +6,261 @@ import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
+import ItemListPeerActionItem
import PresentationDataUtils
import AccountContext
+import PromptUI
+import UndoUI
private final class WinterGramSettingsArguments {
let updateSettings: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void
- let presentSendWithoutSound: () -> Void
- let presentPeerId: () -> Void
- let presentTranslationProvider: () -> Void
- let presentWebviewPlatform: () -> Void
- let presentIconPack: () -> Void
+ let toggleDropdown: (WinterGramDropdown) -> Void
+ let selectDropdownOption: (WinterGramDropdown, Int) -> Void
+ let toggleGhostExpanded: () -> Void
+ let editSpoofDevice: () -> Void
+ let addSpoofTemplate: () -> Void
+ let toggleSpoofTemplateSelected: (Int) -> Void
+ let deleteSelectedSpoofTemplates: () -> Void
+ let editApiId: () -> Void
+ let editApiHash: () -> Void
+ let openStash: () -> Void
+ let editStashPasscode: () -> Void
+ let editDeletedMark: () -> Void
+ let context: AccountContext
+ let openUrl: (String) -> Void
+ let clearDeleted: () -> Void
init(
+ context: AccountContext,
updateSettings: @escaping (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void,
- presentSendWithoutSound: @escaping () -> Void,
- presentPeerId: @escaping () -> Void,
- presentTranslationProvider: @escaping () -> Void,
- presentWebviewPlatform: @escaping () -> Void,
- presentIconPack: @escaping () -> Void
+ toggleDropdown: @escaping (WinterGramDropdown) -> Void,
+ selectDropdownOption: @escaping (WinterGramDropdown, Int) -> Void,
+ toggleGhostExpanded: @escaping () -> Void,
+ editSpoofDevice: @escaping () -> Void,
+ addSpoofTemplate: @escaping () -> Void,
+ toggleSpoofTemplateSelected: @escaping (Int) -> Void,
+ deleteSelectedSpoofTemplates: @escaping () -> Void,
+ editApiId: @escaping () -> Void,
+ editApiHash: @escaping () -> Void,
+ openStash: @escaping () -> Void,
+ editStashPasscode: @escaping () -> Void,
+ editDeletedMark: @escaping () -> Void,
+ openUrl: @escaping (String) -> Void,
+ clearDeleted: @escaping () -> Void
) {
self.updateSettings = updateSettings
- self.presentSendWithoutSound = presentSendWithoutSound
- self.presentPeerId = presentPeerId
- self.presentTranslationProvider = presentTranslationProvider
- self.presentWebviewPlatform = presentWebviewPlatform
- self.presentIconPack = presentIconPack
+ self.toggleDropdown = toggleDropdown
+ self.selectDropdownOption = selectDropdownOption
+ self.toggleGhostExpanded = toggleGhostExpanded
+ self.editSpoofDevice = editSpoofDevice
+ self.addSpoofTemplate = addSpoofTemplate
+ self.toggleSpoofTemplateSelected = toggleSpoofTemplateSelected
+ self.deleteSelectedSpoofTemplates = deleteSelectedSpoofTemplates
+ self.editApiId = editApiId
+ self.editApiHash = editApiHash
+ self.openStash = openStash
+ self.editStashPasscode = editStashPasscode
+ self.editDeletedMark = editDeletedMark
+ self.context = context
+ self.openUrl = openUrl
+ self.clearDeleted = clearDeleted
}
}
-private enum WinterGramSettingsSection: Int32 {
+public enum WinterGramSettingsSection: Int32, CaseIterable {
+ case banner
case ghost
case history
case stash
case antiFeatures
- case confirmations
case chat
- case appearance
case liquidGlass
+ case spoofing
+ // Combined "super-categories" shown in the redesigned main menu.
+ case ayugram // Ghost Mode + History + Hidden Archive
+ case other // Chat tweaks + Spoofing (show id, registration date, spoofer, …)
+
+ public var title: String {
+ switch self {
+ case .banner:
+ return ""
+ case .ghost:
+ return "Ghost Mode"
+ case .history:
+ return "History"
+ case .stash:
+ return "Hidden Archive"
+ case .antiFeatures:
+ return "Features"
+ case .chat:
+ return "Chat"
+ case .liquidGlass:
+ return "Liquid Glass"
+ case .spoofing:
+ return "Spoofing"
+ case .ayugram:
+ return "Core"
+ case .other:
+ return "Other"
+ }
+ }
+
+ public var iconName: String {
+ // SF Symbols used for the main menu cells.
+ switch self {
+ case .banner:
+ return ""
+ case .ghost:
+ return "eye.slash"
+ case .history:
+ return "clock.arrow.circlepath"
+ case .stash:
+ return "archivebox"
+ case .antiFeatures:
+ return "sparkles"
+ case .chat:
+ return "message"
+ case .liquidGlass:
+ return "drop"
+ case .spoofing:
+ return "theatermasks"
+ case .ayugram:
+ return "shield.fill"
+ case .other:
+ return "ellipsis.circle"
+ }
+ }
+
+ // Maps a deep-link path/section name (wnt://wintergram/) to a settings subtab.
+ public init?(deepLinkName: String) {
+ switch deepLinkName.lowercased() {
+ case "ghost", "ghostmode": self = .ghost
+ case "history": self = .history
+ case "stash", "archive", "hiddenarchive": self = .stash
+ case "features", "antifeatures", "anti": self = .antiFeatures
+ case "chat": self = .chat
+ case "glass", "liquidglass": self = .liquidGlass
+ case "spoofing", "spoof": self = .spoofing
+ case "ayugram", "ayu": self = .ayugram
+ case "other", "misc": self = .other
+ default: return nil
+ }
+ }
}
+private enum WinterGramDropdown: Equatable {
+ case sendWithoutSound
+ case peerId
+ case translationProvider
+ case webviewPlatform
+ case stashPrivacy
+}
+
+// Single source of truth for each inline dropdown's options: display title (English; localized at
+// render time), whether it is the current selection, and how to apply it.
+private func winterGramDropdownOptions(_ dropdown: WinterGramDropdown, settings: WinterGramSettings) -> [(title: String, selected: Bool, apply: (WinterGramSettings) -> WinterGramSettings)] {
+ switch dropdown {
+ case .stashPrivacy:
+ let p = settings.stashPrivacy
+ return [
+ ("Profile Photo", p.profilePhoto, { s in var s = s; s.stashPrivacy.profilePhoto = !s.stashPrivacy.profilePhoto; return s }),
+ ("Phone Number", p.phoneNumber, { s in var s = s; s.stashPrivacy.phoneNumber = !s.stashPrivacy.phoneNumber; return s }),
+ ("Last Seen", p.presence, { s in var s = s; s.stashPrivacy.presence = !s.stashPrivacy.presence; return s }),
+ ("Forwards", p.forwards, { s in var s = s; s.stashPrivacy.forwards = !s.stashPrivacy.forwards; return s }),
+ ("Voice Calls", p.voiceCalls, { s in var s = s; s.stashPrivacy.voiceCalls = !s.stashPrivacy.voiceCalls; return s }),
+ ("Birthday", p.birthday, { s in var s = s; s.stashPrivacy.birthday = !s.stashPrivacy.birthday; return s }),
+ ("Gifts Auto-Save", p.giftsAutoSave, { s in var s = s; s.stashPrivacy.giftsAutoSave = !s.stashPrivacy.giftsAutoSave; return s }),
+ ("Bio", p.bio, { s in var s = s; s.stashPrivacy.bio = !s.stashPrivacy.bio; return s }),
+ ("Saved Music", p.savedMusic, { s in var s = s; s.stashPrivacy.savedMusic = !s.stashPrivacy.savedMusic; return s }),
+ ("Group Invitations", p.groupInvitations, { s in var s = s; s.stashPrivacy.groupInvitations = !s.stashPrivacy.groupInvitations; return s })
+ ]
+ case .sendWithoutSound:
+ let v = settings.sendWithoutSound
+ return [
+ ("Never", v == .never, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.sendWithoutSound = .never; return s }),
+ ("In Ghost Mode", v == .inGhostMode, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.sendWithoutSound = .inGhostMode; return s }),
+ ("Always", v == .always, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.sendWithoutSound = .always; return s })
+ ]
+ case .peerId:
+ let v = settings.showPeerId
+ return [
+ ("Hidden", v == .hidden, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.showPeerId = .hidden; return s }),
+ ("Telegram API", v == .telegramApi, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.showPeerId = .telegramApi; return s }),
+ ("Bot API", v == .botApi, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.showPeerId = .botApi; return s })
+ ]
+ case .translationProvider:
+ let v = settings.translationProvider
+ return [
+ ("Disabled", !settings.translateMessages, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.translateMessages = false; return s }),
+ ("Telegram", settings.translateMessages && v == .telegram, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.translateMessages = true; s.translationProvider = .telegram; return s }),
+ ("Google", settings.translateMessages && v == .google, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.translateMessages = true; s.translationProvider = .google; return s }),
+ ("Yandex", settings.translateMessages && v == .yandex, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.translateMessages = true; s.translationProvider = .yandex; return s }),
+ ("System", settings.translateMessages && v == .system, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.translateMessages = true; s.translationProvider = .system; return s })
+ ]
+ case .webviewPlatform:
+ let v = settings.webviewSpoofPlatform
+ return [
+ ("Automatic", v == .auto, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.webviewSpoofPlatform = .auto; return s }),
+ ("iOS", v == .ios, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.webviewSpoofPlatform = .ios; return s }),
+ ("Android", v == .android, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.webviewSpoofPlatform = .android; return s }),
+ ("macOS", v == .macos, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.webviewSpoofPlatform = .macos; return s }),
+ ("Desktop", v == .desktop, { (s: WinterGramSettings) -> WinterGramSettings in var s = s; s.webviewSpoofPlatform = .desktop; return s })
+ ]
+ }
+}
+
+// Built-in device-model spoof presets shown as tappable cards in the Spoofing section.
+// `model` is the string reported to Telegram (nil = the device's real model). `subtitle` describes it.
+private let winterGramDevicePresets: [(name: String, subtitle: String, model: String?)] = [
+ ("Real device", "Report this device's real model", nil),
+ ("iPhone 16 Pro Max", "iPhone17,2 · A18 Pro", "iPhone 16 Pro Max"),
+ ("iPhone 15 Pro Max", "iPhone16,2 · A17 Pro", "iPhone 15 Pro Max"),
+ ("iPhone 15 Pro", "iPhone16,1 · A17 Pro", "iPhone 15 Pro"),
+ ("iPhone 15", "iPhone15,4 · A16", "iPhone 15"),
+ ("iPhone 14 Pro Max", "iPhone15,3 · A16", "iPhone 14 Pro Max"),
+ ("iPhone 14", "iPhone14,7 · A15", "iPhone 14"),
+ ("iPhone 13 Pro", "iPhone14,2 · A15", "iPhone 13 Pro"),
+ ("iPhone 13 mini", "iPhone14,4 · A15", "iPhone 13 mini"),
+ ("iPhone 12", "iPhone13,2 · A14", "iPhone 12"),
+ ("iPhone SE (3rd gen)", "iPhone14,6 · A15", "iPhone SE (3rd generation)"),
+ ("iPhone X", "iPhone10,3 · A11", "iPhone X"),
+ ("iPad Pro M4", "iPad16,3 · Apple M4", "iPad Pro M4"),
+ ("Google Pixel 10 Pro", "Pixel 10 Pro · Tensor G5", "Pixel 10 Pro"),
+ ("Samsung Galaxy S25 Ultra", "SM-S938U · Snapdragon 8 Elite", "Samsung Galaxy S25 Ultra"),
+ ("OnePlus 13", "CPH2649 · Snapdragon 8 Elite", "OnePlus 13"),
+ ("Windows 11", "Windows NT 10.0 · x64", "Windows 11"),
+ ("Windows 10", "Windows NT 10.0 · x64", "Windows 10"),
+ ("Linux Desktop", "Linux · x86_64", "Linux Desktop"),
+ ("macOS Sequoia", "Mac15,9 · Apple M3 Max", "macOS Sequoia")
+]
+
private enum WinterGramSettingsEntry: ItemListNodeEntry {
+ case banner
+
case ghostHeader
- case ghostEnabled(Bool)
- case ghostReadReceipts(Bool)
- case ghostReadStories(Bool)
- case ghostOnlineStatus(Bool)
- case ghostUploadProgress(Bool)
- case ghostOfflineAfterOnline(Bool)
- case ghostMarkReadAfterAction(Bool)
- case ghostUseScheduled(Bool)
+ case ghostExpandable(Bool, Bool, [ItemListExpandableSwitchItem.SubItem])
case ghostSendWithoutSound(String)
- case ghostSuggestBeforeStory(Bool)
+ case ghostUseScheduledMessages(Bool)
+ case ghostConfirmStory(Bool)
+ case ghostMarkReadAfterAction(Bool)
case ghostFooter
case historyHeader
+ case historyHeaderSpy
case historySaveDeleted(Bool)
case historySaveEdits(Bool)
+ case historySaveForBots(Bool)
+ case historySaveSelfDestruct(Bool)
case historySemiTransparent(Bool)
+ case historyDeletedMark(String)
+ case historyShowDeletedTime(Bool)
case historyFooter
case stashHeader
+ case stashList(Int)
case stashMute(Bool)
case stashAutoRead(Bool)
+ case stashPasscodeRow(String)
case stashFooter
case antiHeader
@@ -76,26 +269,24 @@ private enum WinterGramSettingsEntry: ItemListNodeEntry {
case antiDisableStories(Bool)
case antiHidePremiumStatuses(Bool)
case antiDisableLinkWarning(Bool)
+ case antiDisableCopyProtection(Bool)
+ case antiAllowScreenshots(Bool)
case antiFooter
- case confirmHeader
case confirmStickers(Bool)
case confirmGif(Bool)
case confirmVoice(Bool)
case chatHeader
- case chatShowSeconds(Bool)
+ case chatPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem])
case chatShowPeerId(String)
- case chatTranslate(Bool)
+ case chatShowRegistrationDate(Bool)
+ case chatHideEditedMark(Bool)
case chatTranslateProvider(String)
- case chatWebviewPlatform(String)
case chatWebviewHeight(Bool)
case chatOnlyAddedEmoji(Bool)
-
- case appearanceHeader
- case appearanceMaterial(Bool)
- case appearanceSingleCorner(Bool)
- case appearanceIconPack(String)
+ case chatForwardWithoutAuthor(Bool)
+ case chatFooter
case glassHeader
case glassEnabled(Bool)
@@ -106,81 +297,137 @@ private enum WinterGramSettingsEntry: ItemListNodeEntry {
case glassBubbles(Bool)
case glassFooter
+ case spoofingHeader
+ case spoofPresetsHeader
+ case spoofPreset(Int, String, String, Bool, Bool)
+ case spoofAddTemplate
+ case spoofDeleteSelected
+ case spoofingDevice(String)
+ case spoofingDevicePreset(Int, Bool)
+ case spoofingWebviewPlatform(String)
+ case spoofingApiId(String)
+ case spoofingApiHash(String)
+ case spoofingFooter
+
+ // Expandable single-select row: dropdown key, title, expanded flag, options.
+ case expandableSelection(WinterGramDropdown, String, Bool, [ItemListExpandableSelectionItem.Option])
+
var section: ItemListSectionId {
switch self {
- case .ghostHeader, .ghostEnabled, .ghostReadReceipts, .ghostReadStories, .ghostOnlineStatus, .ghostUploadProgress, .ghostOfflineAfterOnline, .ghostMarkReadAfterAction, .ghostUseScheduled, .ghostSendWithoutSound, .ghostSuggestBeforeStory, .ghostFooter:
+ case .banner:
+ return WinterGramSettingsSection.banner.rawValue
+ case .ghostHeader, .ghostExpandable, .ghostSendWithoutSound, .ghostUseScheduledMessages, .ghostConfirmStory, .ghostMarkReadAfterAction, .ghostFooter:
return WinterGramSettingsSection.ghost.rawValue
- case .historyHeader, .historySaveDeleted, .historySaveEdits, .historySemiTransparent, .historyFooter:
+ case .expandableSelection(let dropdown, _, _, _):
+ switch dropdown {
+ case .sendWithoutSound:
+ return WinterGramSettingsSection.ghost.rawValue
+ case .peerId:
+ return WinterGramSettingsSection.chat.rawValue
+ case .translationProvider:
+ return WinterGramSettingsSection.chat.rawValue
+ case .webviewPlatform:
+ return WinterGramSettingsSection.spoofing.rawValue
+ case .stashPrivacy:
+ return WinterGramSettingsSection.stash.rawValue
+ }
+ case .historyHeader, .historyHeaderSpy, .historySaveDeleted, .historySaveEdits, .historySaveForBots, .historySaveSelfDestruct, .historySemiTransparent, .historyDeletedMark, .historyShowDeletedTime, .historyFooter:
return WinterGramSettingsSection.history.rawValue
- case .stashHeader, .stashMute, .stashAutoRead, .stashFooter:
+ case .stashHeader, .stashList, .stashMute, .stashAutoRead, .stashPasscodeRow, .stashFooter:
return WinterGramSettingsSection.stash.rawValue
- case .antiHeader, .antiDisableAds, .antiLocalPremium, .antiDisableStories, .antiHidePremiumStatuses, .antiDisableLinkWarning, .antiFooter:
+ case .antiHeader, .antiDisableAds, .antiLocalPremium, .antiDisableStories, .antiHidePremiumStatuses, .antiDisableLinkWarning, .antiDisableCopyProtection, .antiAllowScreenshots, .antiFooter:
return WinterGramSettingsSection.antiFeatures.rawValue
- case .confirmHeader, .confirmStickers, .confirmGif, .confirmVoice:
- return WinterGramSettingsSection.confirmations.rawValue
- case .chatHeader, .chatShowSeconds, .chatShowPeerId, .chatTranslate, .chatTranslateProvider, .chatWebviewPlatform, .chatWebviewHeight, .chatOnlyAddedEmoji:
+ case .confirmStickers, .confirmGif, .confirmVoice:
+ return WinterGramSettingsSection.chat.rawValue
+ case .chatHeader, .chatPreview, .chatShowPeerId, .chatShowRegistrationDate, .chatHideEditedMark, .chatTranslateProvider, .chatWebviewHeight, .chatOnlyAddedEmoji, .chatForwardWithoutAuthor, .chatFooter:
return WinterGramSettingsSection.chat.rawValue
- case .appearanceHeader, .appearanceMaterial, .appearanceSingleCorner, .appearanceIconPack:
- return WinterGramSettingsSection.appearance.rawValue
case .glassHeader, .glassEnabled, .glassVibrancy, .glassChatList, .glassNavBars, .glassTabBar, .glassBubbles, .glassFooter:
return WinterGramSettingsSection.liquidGlass.rawValue
+ case .spoofingHeader, .spoofingDevice, .spoofingDevicePreset, .spoofingWebviewPlatform, .spoofingApiId, .spoofingApiHash, .spoofPresetsHeader, .spoofPreset, .spoofAddTemplate, .spoofDeleteSelected, .spoofingFooter:
+ return WinterGramSettingsSection.spoofing.rawValue
}
}
var stableId: Int32 {
+ // Device preset cards nest between spoofingDevice (30000) and WebView platform (40000).
+ if case let .spoofingDevicePreset(index, _) = self {
+ return 30100 + Int32(index)
+ }
+ return self.baseStableId * 100
+ }
+
+ private var baseStableId: Int32 {
switch self {
+ case .banner: return -1
case .ghostHeader: return 0
- case .ghostEnabled: return 1
- case .ghostReadReceipts: return 2
- case .ghostReadStories: return 3
- case .ghostOnlineStatus: return 4
- case .ghostUploadProgress: return 5
- case .ghostOfflineAfterOnline: return 6
- case .ghostMarkReadAfterAction: return 7
- case .ghostUseScheduled: return 8
- case .ghostSendWithoutSound: return 9
- case .ghostSuggestBeforeStory: return 10
- case .ghostFooter: return 11
- case .historyHeader: return 12
- case .historySaveDeleted: return 13
- case .historySaveEdits: return 14
- case .historySemiTransparent: return 15
- case .historyFooter: return 16
- case .stashHeader: return 17
- case .stashMute: return 18
- case .stashAutoRead: return 19
- case .stashFooter: return 20
- case .antiHeader: return 21
- case .antiDisableAds: return 22
- case .antiLocalPremium: return 23
- case .antiDisableStories: return 24
- case .antiHidePremiumStatuses: return 25
- case .antiDisableLinkWarning: return 26
- case .antiFooter: return 27
- case .confirmHeader: return 28
- case .confirmStickers: return 29
- case .confirmGif: return 30
- case .confirmVoice: return 31
- case .chatHeader: return 32
- case .chatShowSeconds: return 33
- case .chatShowPeerId: return 34
- case .chatTranslate: return 35
- case .chatTranslateProvider: return 36
- case .chatWebviewPlatform: return 37
- case .chatWebviewHeight: return 38
- case .chatOnlyAddedEmoji: return 39
- case .appearanceHeader: return 40
- case .appearanceMaterial: return 41
- case .appearanceSingleCorner: return 42
- case .appearanceIconPack: return 43
- case .glassHeader: return 44
- case .glassEnabled: return 45
- case .glassVibrancy: return 46
- case .glassChatList: return 47
- case .glassNavBars: return 48
- case .glassTabBar: return 49
- case .glassBubbles: return 50
- case .glassFooter: return 51
+ case .ghostExpandable: return 2
+ case .ghostSendWithoutSound: return 3
+ case .ghostUseScheduledMessages: return 4
+ case .ghostConfirmStory: return 5
+ case .ghostMarkReadAfterAction: return 6
+ case .ghostFooter: return 7
+ case .historyHeader: return 10
+ case .historyHeaderSpy: return 11
+ case .historySaveDeleted: return 12
+ case .historySaveEdits: return 13
+ case .historySaveForBots: return 14
+ case .historySaveSelfDestruct: return 15
+ case .historySemiTransparent: return 16
+ case .historyDeletedMark: return 17
+ case .historyShowDeletedTime: return 18
+ case .historyFooter: return 19
+ case .stashHeader: return 20
+ case .stashList: return 21
+ case .stashMute: return 23
+ case .stashAutoRead: return 24
+ case .stashPasscodeRow: return 25
+ case .stashFooter: return 37
+ case .antiHeader: return 30
+ case .antiDisableAds: return 31
+ case .antiLocalPremium: return 32
+ case .antiDisableStories: return 33
+ case .antiHidePremiumStatuses: return 34
+ case .antiDisableLinkWarning: return 35
+ case .antiDisableCopyProtection: return 36
+ case .antiAllowScreenshots: return 37
+ case .antiFooter: return 38
+ case .confirmStickers: return 64
+ case .confirmGif: return 65
+ case .confirmVoice: return 66
+ case .chatHeader: return 48
+ case .chatPreview: return 49
+ case .chatShowPeerId: return 52
+ case .chatShowRegistrationDate: return 53
+ case .chatHideEditedMark: return 54
+ case .chatTranslateProvider: return 56
+ case .chatWebviewHeight: return 58
+ case .chatOnlyAddedEmoji: return 59
+ case .chatForwardWithoutAuthor: return 60
+ case .chatFooter: return 67
+ case .glassHeader: return 80
+ case .glassEnabled: return 81
+ case .glassVibrancy: return 82
+ case .glassChatList: return 83
+ case .glassNavBars: return 84
+ case .glassTabBar: return 85
+ case .glassBubbles: return 86
+ case .glassFooter: return 87
+ case .spoofingHeader: return 100
+ case .spoofPresetsHeader: return 101
+ case let .spoofPreset(index, _, _, _, _): return 102 + Int32(index)
+ case .spoofAddTemplate: return 203
+ case .spoofDeleteSelected: return 204
+ case .spoofingDevice: return 300
+ case .spoofingDevicePreset: return 301
+ case .spoofingWebviewPlatform: return 400
+ case .spoofingApiId: return 401
+ case .spoofingApiHash: return 402
+ case .spoofingFooter: return 403
+ case .expandableSelection(.sendWithoutSound, _, _, _): return 3
+ case .expandableSelection(.peerId, _, _, _): return 52
+ case .expandableSelection(.translationProvider, _, _, _): return 56
+ case .expandableSelection(.webviewPlatform, _, _, _): return 400
+ case .expandableSelection(.stashPrivacy, _, _, _): return 26
}
}
@@ -190,196 +437,287 @@ private enum WinterGramSettingsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! WinterGramSettingsArguments
+ let lang = presentationData.strings
switch self {
+ case .banner:
+ return WinterGramBannerItem(theme: presentationData.theme, title: "WinterGram", subtitle: "", iconImage: UIImage(bundleImageName: "WinterGramDark"), sectionId: self.section)
case .ghostHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "GHOST MODE", sectionId: self.section)
- case let .ghostEnabled(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Ghost Mode", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.ghostModeEnabled = value; return s }
- })
- case let .ghostReadReceipts(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Send Read Receipts", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.sendReadReceipts = value; return s }
- })
- case let .ghostReadStories(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Send Story Views", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.sendReadStories = value; return s }
- })
- case let .ghostOnlineStatus(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Send Online Status", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.sendOnlineStatus = value; return s }
- })
- case let .ghostUploadProgress(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Send Typing & Upload Status", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.sendUploadProgress = value; return s }
- })
- case let .ghostOfflineAfterOnline(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Go Offline After Online", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.sendOfflineAfterOnline = value; return s }
- })
- case let .ghostMarkReadAfterAction(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Mark Read After Action", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.markReadAfterAction = value; return s }
- })
- case let .ghostUseScheduled(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Use Scheduled Messages", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.useScheduledMessages = value; return s }
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_GHOSTMODE, sectionId: self.section)
+ case let .ghostExpandable(value, isExpanded, subItems):
+ return ItemListExpandableSwitchItem(presentationData: presentationData, systemStyle: .glass, title: lang.WinterGram_GhostMode, value: value, isExpanded: isExpanded, subItems: subItems, sectionId: self.section, style: .blocks, updated: { newValue in
+ arguments.updateSettings { var s = $0; s.ghostModeEnabled = newValue; return s }
+ }, selectAction: {
+ arguments.toggleGhostExpanded()
+ }, subAction: { subItem in
+ guard let id = subItem.id as? String else {
+ return
+ }
+ arguments.updateSettings { settings in
+ var s = settings
+ switch id {
+ case "readMessages":
+ s.sendReadReceipts = !subItem.isSelected
+ case "readStories":
+ s.sendReadStories = !subItem.isSelected
+ case "online":
+ s.sendOnlineStatus = !subItem.isSelected
+ case "typing":
+ s.sendUploadProgress = !subItem.isSelected
+ case "autoOffline":
+ s.sendOfflineAfterOnline = subItem.isSelected
+ default:
+ break
+ }
+ s.ghostModeEnabled = true
+ return s
+ }
})
case let .ghostSendWithoutSound(value):
- return ItemListDisclosureItem(presentationData: presentationData, title: "Send Without Sound", label: value, sectionId: self.section, style: .blocks, action: {
- arguments.presentSendWithoutSound()
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_SendWithoutSound, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.toggleDropdown(.sendWithoutSound)
})
- case let .ghostSuggestBeforeStory(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Ask Before Viewing Stories", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.suggestGhostBeforeStory = value; return s }
+ case let .ghostUseScheduledMessages(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_UseScheduledMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.useScheduledMessages = value; return s }
})
+ case let .ghostConfirmStory(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ConfirmStoryView, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.confirmStoryView = value; return s }
+ })
+ case let .ghostMarkReadAfterAction(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ReadAfterAction, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.markReadAfterAction = value; return s }
+ })
+
case .ghostFooter:
- return ItemListTextItem(presentationData: presentationData, text: .plain("When Ghost Mode is on, WinterGram stops sending read receipts, online status and typing activity."), sectionId: self.section)
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_WhenGhostModeIsOnWinterGramStopsSendingReadReceiptsOnlineStatusAndTypingActivity), sectionId: self.section)
case .historyHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "HISTORY", sectionId: self.section)
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_HISTORY, sectionId: self.section)
+ case .historyHeaderSpy:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_SPYMODE, sectionId: self.section)
case let .historySaveDeleted(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Save Deleted Messages", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_SaveDeletedMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.saveDeletedMessages = value; return s }
})
case let .historySaveEdits(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Save Edit History", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_SaveEditHistory, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.saveMessagesHistory = value; return s }
})
+ case let .historySaveForBots(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_SaveDeletedFromBots, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.saveForBots = value; return s }
+ })
+ case let .historySaveSelfDestruct(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_SaveSelfDestructMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.saveSelfDestructMessages = value; return s }
+ })
case let .historySemiTransparent(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Dim Deleted Messages", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_DimDeletedMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.semiTransparentDeletedMessages = value; return s }
})
+ case let .historyDeletedMark(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_DeletedMark, label: value.isEmpty ? "🧹" : value, sectionId: self.section, style: .blocks, action: {
+ arguments.editDeletedMark()
+ })
+ case let .historyShowDeletedTime(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ShowDeletionTime, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.showDeletedTime = value; return s }
+ })
case .historyFooter:
- return ItemListTextItem(presentationData: presentationData, text: .plain("Deleted and edited messages are kept locally on this device only."), sectionId: self.section)
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_DeletedAndEditedMessagesAreKeptLocallyOnThisDeviceOnly), sectionId: self.section)
case .stashHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "HIDDEN ARCHIVE", sectionId: self.section)
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_HIDDENARCHIVE, sectionId: self.section)
+ case let .stashList(count):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_StashedChats, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: {
+ arguments.openStash()
+ })
case let .stashMute(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Mute Notifications", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_MuteNotifications, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.stashMuteNotifications = value; return s }
})
case let .stashAutoRead(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Auto Mark as Read", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_AutoMarkAsRead, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.stashAutoMarkRead = value; return s }
})
+ case let .stashPasscodeRow(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_StashPasscode, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.editStashPasscode()
+ })
case .stashFooter:
- return ItemListTextItem(presentationData: presentationData, text: .plain("Stashed chats are hidden from the main list and accessible only here."), sectionId: self.section)
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_StashedChatsAreHiddenFromTheMainListAndAccessibleOnlyHere), sectionId: self.section)
case .antiHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "FEATURES", sectionId: self.section)
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_FEATURES, sectionId: self.section)
case let .antiDisableAds(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Disable Ads", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_DisableAds, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.disableAds = value; return s }
})
case let .antiLocalPremium(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Local Premium", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_LocalPremium, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.localPremium = value; return s }
})
case let .antiDisableStories(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Hide Stories", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_HideStories, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.disableStories = value; return s }
})
case let .antiHidePremiumStatuses(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Hide Premium Statuses", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_HidePremiumStatuses, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.hidePremiumStatuses = value; return s }
})
case let .antiDisableLinkWarning(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Disable Open Link Warning", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_DisableOpenLinkWarning, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.disableOpenLinkWarning = value; return s }
})
+ case let .antiDisableCopyProtection(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_AllowSavingRestrictedContent, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.disableCopyProtection = value; return s }
+ })
+ case let .antiAllowScreenshots(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_AllowScreenshotsEverywhere, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.allowScreenshots = value; return s }
+ })
case .antiFooter:
- return ItemListTextItem(presentationData: presentationData, text: .plain("Local Premium unlocks Premium-only UI on this device; it does not grant server-side Premium."), sectionId: self.section)
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_LocalPremiumUnlocksPremiumOnlyUIOnThisDeviceItDoesNotGrantServerSidePremium), sectionId: self.section)
- case .confirmHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "SEND CONFIRMATIONS", sectionId: self.section)
case let .confirmStickers(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Confirm Stickers", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ConfirmStickers, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.stickerConfirmation = value; return s }
})
case let .confirmGif(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Confirm GIFs", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ConfirmGIFs, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.gifConfirmation = value; return s }
})
case let .confirmVoice(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Confirm Voice Messages", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ConfirmVoiceMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.voiceConfirmation = value; return s }
})
case .chatHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "CHAT", sectionId: self.section)
- case let .chatShowSeconds(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Show Message Seconds", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.showMessageSeconds = value; return s }
- })
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_CHAT, sectionId: self.section)
+ case let .chatPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, messageItems):
+ return ThemeSettingsChatPreviewItem(context: arguments.context, systemStyle: .glass, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: messageItems)
case let .chatShowPeerId(value):
- return ItemListDisclosureItem(presentationData: presentationData, title: "Show Peer ID", label: value, sectionId: self.section, style: .blocks, action: {
- arguments.presentPeerId()
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_ShowPeerID, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.toggleDropdown(.peerId)
})
- case let .chatTranslate(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Message Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.translateMessages = value; return s }
+ case let .chatShowRegistrationDate(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ShowRegistrationDate, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.showRegistrationDate = value; return s }
+ })
+ case let .chatHideEditedMark(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_HideEditedMark, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.hideEditedMark = value; return s }
})
case let .chatTranslateProvider(value):
- return ItemListDisclosureItem(presentationData: presentationData, title: "Translation Provider", label: value, sectionId: self.section, style: .blocks, action: {
- arguments.presentTranslationProvider()
- })
- case let .chatWebviewPlatform(value):
- return ItemListDisclosureItem(presentationData: presentationData, title: "WebView Platform", label: value, sectionId: self.section, style: .blocks, action: {
- arguments.presentWebviewPlatform()
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_TranslationProvider, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.toggleDropdown(.translationProvider)
})
case let .chatWebviewHeight(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Increase WebView Height", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_IncreaseWebViewHeight, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.increaseWebviewHeight = value; return s }
})
case let .chatOnlyAddedEmoji(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Only Added Emoji & Stickers", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_OnlyAddedEmojiStickers, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.showOnlyAddedEmojisAndStickers = value; return s }
})
-
- case .appearanceHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "APPEARANCE", sectionId: self.section)
- case let .appearanceMaterial(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Material Design", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.materialDesign = value; return s }
- })
- case let .appearanceSingleCorner(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Single Corner Radius", value: value, sectionId: self.section, style: .blocks, updated: { value in
- arguments.updateSettings { var s = $0; s.singleCornerRadius = value; return s }
- })
- case let .appearanceIconPack(value):
- return ItemListDisclosureItem(presentationData: presentationData, title: "Icon Pack", label: value, sectionId: self.section, style: .blocks, action: {
- arguments.presentIconPack()
+ case let .chatForwardWithoutAuthor(value):
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ForwardWithoutAuthor, value: value, sectionId: self.section, style: .blocks, updated: { value in
+ arguments.updateSettings { var s = $0; s.forwardWithoutAuthor = value; return s }
})
+ case .chatFooter:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_TheseOptionsChangeHowMessageMetadataAndActionsAreShownInChats), sectionId: self.section)
case .glassHeader:
- return ItemListSectionHeaderItem(presentationData: presentationData, text: "LIQUID GLASS", sectionId: self.section)
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_LIQUIDGLASS, sectionId: self.section)
case let .glassEnabled(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Liquid Glass", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_LiquidGlass, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.enabled = value; return s }
})
case let .glassVibrancy(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Vibrancy", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_Vibrancy, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.vibrancy = value; return s }
})
case let .glassChatList(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Apply to Chat List", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ApplyToChatList, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.applyToChatList = value; return s }
})
case let .glassNavBars(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Apply to Navigation Bars", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ApplyToNavigationBars, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.applyToNavigationBars = value; return s }
})
case let .glassTabBar(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Apply to Tab Bar", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ApplyToTabBar, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.applyToTabBar = value; return s }
})
case let .glassBubbles(value):
- return ItemListSwitchItem(presentationData: presentationData, title: "Apply to Bubbles", value: value, sectionId: self.section, style: .blocks, updated: { value in
+ return ItemListSwitchItem(presentationData: presentationData, title: lang.WinterGram_ApplyToBubbles, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { var s = $0; s.liquidGlass.applyToBubbles = value; return s }
})
case .glassFooter:
- return ItemListTextItem(presentationData: presentationData, text: .plain("Transparency, blur and tint can be fine-tuned per surface. Turn Liquid Glass off for the standard opaque look."), sectionId: self.section)
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_TransparencyBlurAndTintCanBeFineTunedPerSurfaceTurnLiquidGlassOffForTheStandardOpaqueLook), sectionId: self.section)
+
+ case .spoofingHeader:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_SPOOFING, sectionId: self.section)
+ case let .spoofingDevice(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_SpoofDeviceModel, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.editSpoofDevice()
+ })
+ case let .spoofingDevicePreset(index, selected):
+ let preset = winterGramDevicePresets[index]
+ return ItemListCheckboxItem(presentationData: presentationData, title: preset.name, subtitle: preset.subtitle, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
+ arguments.updateSettings { var s = $0; s.spoofDeviceModel = preset.model; return s }
+ })
+ case let .spoofingWebviewPlatform(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_WebViewPlatform, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.toggleDropdown(.webviewPlatform)
+ })
+ case let .spoofingApiId(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_APIID, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.editApiId()
+ })
+ case let .spoofingApiHash(value):
+ return ItemListDisclosureItem(presentationData: presentationData, title: lang.WinterGram_APIHash, label: wntOption(value, lang), sectionId: self.section, style: .blocks, action: {
+ arguments.editApiHash()
+ })
+ case .spoofPresetsHeader:
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: lang.WinterGram_Templates, sectionId: self.section)
+ case let .spoofPreset(index, name, subtitle, editing, selected):
+ if editing {
+ return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: name, subtitle: subtitle, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
+ arguments.toggleSpoofTemplateSelected(index)
+ })
+ } else {
+ return ItemListDisclosureItem(presentationData: presentationData, title: name, label: subtitle, labelStyle: .detailText, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
+ arguments.updateSettings { settings in
+ var settings = settings
+ if index < settings.spoofPresets.count {
+ let preset = settings.spoofPresets[index]
+ settings.spoofDeviceModel = preset.deviceModel.isEmpty ? nil : preset.deviceModel
+ settings.spoofAppVersion = preset.appVersion.isEmpty ? nil : preset.appVersion
+ }
+ return settings
+ }
+ })
+ }
+ case .spoofAddTemplate:
+ return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: lang.WinterGram_AddTemplate, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
+ arguments.addSpoofTemplate()
+ })
+ case .spoofDeleteSelected:
+ return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: lang.WinterGram_DeleteSelected, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
+ arguments.deleteSelectedSpoofTemplates()
+ })
+ case .spoofingFooter:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(lang.WinterGram_SpoofTheDeviceModelAppVersionAndWebViewPlatformReportedToTelegramAndMiniAppsAPIIDHashUseYourOwnCredentialsFromMyTelegramOrgChangingThemRequiresReLogin), sectionId: self.section)
+ case let .expandableSelection(dropdown, title, isExpanded, options):
+ let mode: ItemListExpandableSelectionItem.SelectionMode = dropdown == .stashPrivacy ? .multiple : .single
+ return ItemListExpandableSelectionItem(presentationData: presentationData, systemStyle: .glass, title: title, options: options, mode: mode, isExpanded: isExpanded, sectionId: self.section, style: .blocks, updated: { option in
+ arguments.selectDropdownOption(dropdown, option.index)
+ }, toggleExpanded: {
+ arguments.toggleDropdown(dropdown)
+ })
}
}
}
@@ -419,189 +757,543 @@ private func webviewPlatformLabel(_ value: WinterGramWebviewPlatform) -> String
}
}
+private func avatarRadiusLabel(_ value: Int32) -> String {
+ switch value {
+ case 50: return "Round"
+ case 30: return "Squircle"
+ case 15: return "Rounded"
+ case 0: return "Square"
+ default: return "\(value)"
+ }
+}
+
+private func spoofLabel(_ value: String?) -> String {
+ if let value = value, !value.isEmpty {
+ return value
+ }
+ return "Default"
+}
+
private func iconPackLabel(_ value: WinterGramIconPack) -> String {
switch value {
case .wintergram: return "WinterGram"
- case .ayugram: return "AyuGram"
+ case .ayugram: return "Ayu"
case .exteragram: return "exteraGram"
case .telegram: return "Telegram"
}
}
-private func winterGramSettingsEntries(settings: WinterGramSettings) -> [WinterGramSettingsEntry] {
+private func winterGramSettingsEntries(presentationData: PresentationData, settings: WinterGramSettings, deletedCount: Int, expandedDropdown: WinterGramDropdown?, ghostExpanded: Bool, spoofTemplatesEditing: Bool, selectedTemplates: Set, category: WinterGramSettingsSection? = nil) -> [WinterGramSettingsEntry] {
+ let lang = presentationData.strings
var entries: [WinterGramSettingsEntry] = []
- entries.append(.ghostHeader)
- entries.append(.ghostEnabled(settings.ghostModeEnabled))
- entries.append(.ghostReadReceipts(settings.sendReadReceipts))
- entries.append(.ghostReadStories(settings.sendReadStories))
- entries.append(.ghostOnlineStatus(settings.sendOnlineStatus))
- entries.append(.ghostUploadProgress(settings.sendUploadProgress))
- entries.append(.ghostOfflineAfterOnline(settings.sendOfflineAfterOnline))
- entries.append(.ghostMarkReadAfterAction(settings.markReadAfterAction))
- entries.append(.ghostUseScheduled(settings.useScheduledMessages))
- entries.append(.ghostSendWithoutSound(sendWithoutSoundLabel(settings.sendWithoutSound)))
- entries.append(.ghostSuggestBeforeStory(settings.suggestGhostBeforeStory))
- entries.append(.ghostFooter)
+ // Appends an expandable single-select row with checkbox options shown inline.
+ func appendDropdown(_ dropdown: WinterGramDropdown, _ title: String, _ section: WinterGramSettingsSection) {
+ let options = winterGramDropdownOptions(dropdown, settings: settings).enumerated().map { index, option in
+ ItemListExpandableSelectionItem.Option(id: "\(dropdown)_\(index)", title: wntOption(option.title, lang), isSelected: option.selected, index: index)
+ }
+ entries.append(.expandableSelection(dropdown, title, expandedDropdown == dropdown, options))
+ }
- entries.append(.historyHeader)
- entries.append(.historySaveDeleted(settings.saveDeletedMessages))
- entries.append(.historySaveEdits(settings.saveMessagesHistory))
- entries.append(.historySemiTransparent(settings.semiTransparentDeletedMessages))
- entries.append(.historyFooter)
+ func appendGhost() {
+ entries.append(.ghostHeader)
+ // Expandable Ghost Mode switch with checkbox sub-items for each suppressed signal.
+ let subItems: [ItemListExpandableSwitchItem.SubItem] = [
+ .init(id: "readMessages", title: lang.WinterGram_DontReadMessages, isSelected: settings.suppressesReadReceipts, isEnabled: true),
+ .init(id: "readStories", title: lang.WinterGram_DontReadStories, isSelected: settings.suppressesStoryViews, isEnabled: true),
+ .init(id: "online", title: lang.WinterGram_DontSendOnline, isSelected: settings.suppressesOnlinePresence, isEnabled: true),
+ .init(id: "typing", title: lang.WinterGram_DontSendTyping, isSelected: settings.suppressesTypingStatus, isEnabled: true),
+ .init(id: "autoOffline", title: lang.WinterGram_AutoOffline, isSelected: settings.sendOfflineAfterOnline, isEnabled: true)
+ ]
+ entries.append(.ghostExpandable(settings.ghostModeEnabled, ghostExpanded, subItems))
+ appendDropdown(.sendWithoutSound, lang.WinterGram_SendWithoutSound, .ghost)
+ entries.append(.ghostUseScheduledMessages(settings.useScheduledMessages))
+ entries.append(.ghostConfirmStory(settings.confirmStoryView))
+ entries.append(.ghostMarkReadAfterAction(settings.markReadAfterAction))
+ entries.append(.ghostFooter)
+ }
- entries.append(.stashHeader)
- entries.append(.stashMute(settings.stashMuteNotifications))
- entries.append(.stashAutoRead(settings.stashAutoMarkRead))
- entries.append(.stashFooter)
+ func appendHistory() {
+ if category == .ayugram {
+ entries.append(.historyHeaderSpy)
+ entries.append(.historySaveDeleted(settings.saveDeletedMessages))
+ entries.append(.historySaveEdits(settings.saveMessagesHistory))
+ entries.append(.historyDeletedMark(settings.deletedMark))
+ entries.append(.historyShowDeletedTime(settings.showDeletedTime))
+ } else {
+ entries.append(.historyHeader)
+ entries.append(.historySaveDeleted(settings.saveDeletedMessages))
+ entries.append(.historySaveEdits(settings.saveMessagesHistory))
+ entries.append(.historySaveForBots(settings.saveForBots))
+ entries.append(.historySaveSelfDestruct(settings.saveSelfDestructMessages))
+ entries.append(.historySemiTransparent(settings.semiTransparentDeletedMessages))
+ entries.append(.historyDeletedMark(settings.deletedMark))
+ entries.append(.historyShowDeletedTime(settings.showDeletedTime))
+ entries.append(.historyFooter)
+ }
+ }
- entries.append(.antiHeader)
- entries.append(.antiDisableAds(settings.disableAds))
- entries.append(.antiLocalPremium(settings.localPremium))
- entries.append(.antiDisableStories(settings.disableStories))
- entries.append(.antiHidePremiumStatuses(settings.hidePremiumStatuses))
- entries.append(.antiDisableLinkWarning(settings.disableOpenLinkWarning))
- entries.append(.antiFooter)
+ func appendStash() {
+ entries.append(.stashHeader)
+ entries.append(.stashList(Set(settings.stashedPeerIds).count))
+ entries.append(.stashMute(settings.stashMuteNotifications))
+ entries.append(.stashAutoRead(settings.stashAutoMarkRead))
+ entries.append(.stashPasscodeRow(settings.stashPasscode.isEmpty ? "None" : "••••"))
+ // All auto-privacy toggles combined into one expandable multi-select checkbox row.
+ appendDropdown(.stashPrivacy, lang.WinterGram_AutoPrivacy, .stash)
+ entries.append(.stashFooter)
+ }
- entries.append(.confirmHeader)
- entries.append(.confirmStickers(settings.stickerConfirmation))
- entries.append(.confirmGif(settings.gifConfirmation))
- entries.append(.confirmVoice(settings.voiceConfirmation))
+ func appendAntiFeatures() {
+ entries.append(.antiHeader)
+ entries.append(.antiDisableAds(settings.disableAds))
+ entries.append(.antiLocalPremium(settings.localPremium))
+ entries.append(.antiDisableStories(settings.disableStories))
+ entries.append(.antiHidePremiumStatuses(settings.hidePremiumStatuses))
+ entries.append(.antiDisableLinkWarning(settings.disableOpenLinkWarning))
+ entries.append(.antiDisableCopyProtection(settings.disableCopyProtection))
+ entries.append(.antiAllowScreenshots(settings.allowScreenshots))
+ entries.append(.antiFooter)
+ }
- entries.append(.chatHeader)
- entries.append(.chatShowSeconds(settings.showMessageSeconds))
- entries.append(.chatShowPeerId(peerIdLabel(settings.showPeerId)))
- entries.append(.chatTranslate(settings.translateMessages))
- entries.append(.chatTranslateProvider(translationProviderLabel(settings.translationProvider)))
- entries.append(.chatWebviewPlatform(webviewPlatformLabel(settings.webviewSpoofPlatform)))
- entries.append(.chatWebviewHeight(settings.increaseWebviewHeight))
- entries.append(.chatOnlyAddedEmoji(settings.showOnlyAddedEmojisAndStickers))
+ func appendChat() {
+ entries.append(.chatHeader)
+ appendDropdown(.peerId, lang.WinterGram_ShowPeerID, .chat)
+ entries.append(.chatShowRegistrationDate(settings.showRegistrationDate))
+ entries.append(.chatHideEditedMark(settings.hideEditedMark))
+ appendDropdown(.translationProvider, lang.WinterGram_MessageTranslation, .chat)
+ entries.append(.chatWebviewHeight(settings.increaseWebviewHeight))
+ entries.append(.chatOnlyAddedEmoji(settings.showOnlyAddedEmojisAndStickers))
+ entries.append(.chatForwardWithoutAuthor(settings.forwardWithoutAuthor))
+ entries.append(.confirmStickers(settings.stickerConfirmation))
+ entries.append(.confirmGif(settings.gifConfirmation))
+ entries.append(.confirmVoice(settings.voiceConfirmation))
+ // Footer last (stableId 67) so the section stays in ascending stableId order.
+ entries.append(.chatFooter)
+ }
- entries.append(.appearanceHeader)
- entries.append(.appearanceMaterial(settings.materialDesign))
- entries.append(.appearanceSingleCorner(settings.singleCornerRadius))
- entries.append(.appearanceIconPack(iconPackLabel(settings.iconPack)))
+ func appendLiquidGlass() {
+ entries.append(.glassHeader)
+ entries.append(.glassEnabled(settings.liquidGlass.enabled))
+ entries.append(.glassVibrancy(settings.liquidGlass.vibrancy))
+ entries.append(.glassChatList(settings.liquidGlass.applyToChatList))
+ entries.append(.glassNavBars(settings.liquidGlass.applyToNavigationBars))
+ entries.append(.glassTabBar(settings.liquidGlass.applyToTabBar))
+ entries.append(.glassBubbles(settings.liquidGlass.applyToBubbles))
+ entries.append(.glassFooter)
+ }
- entries.append(.glassHeader)
- entries.append(.glassEnabled(settings.liquidGlass.enabled))
- entries.append(.glassVibrancy(settings.liquidGlass.vibrancy))
- entries.append(.glassChatList(settings.liquidGlass.applyToChatList))
- entries.append(.glassNavBars(settings.liquidGlass.applyToNavigationBars))
- entries.append(.glassTabBar(settings.liquidGlass.applyToTabBar))
- entries.append(.glassBubbles(settings.liquidGlass.applyToBubbles))
- entries.append(.glassFooter)
+ func appendSpoofing() {
+ entries.append(.spoofingHeader)
+ // Saved spoof templates at the top of the Spoofing section.
+ entries.append(.spoofPresetsHeader)
+ for (index, preset) in settings.spoofPresets.enumerated() {
+ let subtitle = [preset.deviceModel, preset.appVersion].filter { !$0.isEmpty }.joined(separator: " · ")
+ entries.append(.spoofPreset(index, preset.name, subtitle.isEmpty ? "Default" : subtitle, spoofTemplatesEditing, selectedTemplates.contains(index)))
+ }
+ entries.append(.spoofAddTemplate)
+ if spoofTemplatesEditing && !selectedTemplates.isEmpty {
+ entries.append(.spoofDeleteSelected)
+ }
+ // Device model is chosen via the preset cards below (incl. "Real device") — no separate prompt row.
+ for (i, preset) in winterGramDevicePresets.enumerated() {
+ entries.append(.spoofingDevicePreset(i, preset.model == settings.spoofDeviceModel))
+ }
+ appendDropdown(.webviewPlatform, lang.WinterGram_WebViewPlatform, .spoofing)
+ entries.append(.spoofingApiId(settings.customApiId.flatMap { $0 == 0 ? nil : "\($0)" } ?? "Default"))
+ entries.append(.spoofingApiHash(spoofLabel(settings.customApiHash)))
+ entries.append(.spoofingFooter)
+ }
+
+ if let category = category {
+ switch category {
+ case .banner:
+ break
+ case .ghost:
+ appendGhost()
+ case .history:
+ appendHistory()
+ case .stash:
+ appendStash()
+ case .antiFeatures:
+ appendAntiFeatures()
+ case .chat:
+ appendChat()
+ case .liquidGlass:
+ appendLiquidGlass()
+ case .spoofing:
+ appendSpoofing()
+ case .ayugram:
+ appendGhost()
+ appendHistory()
+ case .other:
+ appendChat()
+ }
+ } else {
+ entries.append(.banner)
+ appendGhost()
+ appendHistory()
+ appendStash()
+ appendAntiFeatures()
+ appendChat()
+ appendLiquidGlass()
+ appendSpoofing()
+ }
return entries
}
-public func winterGramSettingsController(context: AccountContext) -> ViewController {
+public func winterGramSettingsController(context: AccountContext, category: WinterGramSettingsSection? = nil) -> ViewController {
let accountManager = context.sharedContext.accountManager
var presentControllerImpl: ((ViewController, Any?) -> Void)?
+ var pushControllerImpl: ((ViewController) -> Void)?
+ var refreshDeletedCount: (() -> Void)?
+
+ // The combined super-categories (.ayugram/.other) are not standalone tabs in the legacy
+ // no-category segmented view.
+ let sectionTabs: [WinterGramSettingsSection] = WinterGramSettingsSection.allCases.filter { $0 != .ayugram && $0 != .other }
+ let selectedCategoryPromise = ValuePromise(category ?? .ghost, ignoreRepeated: true)
+
+ let initialSettings = currentWinterGramSettings
+ let requiresRestartPromise = ValuePromise(false)
let updateSettings: (@escaping (WinterGramSettings) -> WinterGramSettings) -> Void = { f in
- let _ = updateWinterGramSettingsInteractively(accountManager: accountManager, f).start()
+ let _ = (updateWinterGramSettingsInteractively(accountManager: accountManager, f)
+ |> deliverOnMainQueue).startStandalone(next: { _ in
+ let newSettings = currentWinterGramSettings
+ let needsRestart = newSettings.spoofDeviceModel != initialSettings.spoofDeviceModel ||
+ newSettings.spoofAppVersion != initialSettings.spoofAppVersion ||
+ newSettings.customApiId != initialSettings.customApiId ||
+ newSettings.customApiHash != initialSettings.customApiHash ||
+ newSettings.materialDesign != initialSettings.materialDesign ||
+ newSettings.customFont != initialSettings.customFont ||
+ newSettings.monoFont != initialSettings.monoFont
+ requiresRestartPromise.set(needsRestart)
+ })
}
- func presentChoice(title: String, options: [(String, T)], apply: @escaping (T, WinterGramSettings) -> WinterGramSettings) {
- let presentationData = context.sharedContext.currentPresentationData.with { $0 }
- let controller = ActionSheetController(presentationData: presentationData)
- var items: [ActionSheetItem] = []
- items.append(ActionSheetTextItem(title: title))
- for (label, value) in options {
- items.append(ActionSheetButtonItem(title: label, action: { [weak controller] in
- controller?.dismissAnimated()
- updateSettings { apply(value, $0) }
- }))
+ // Applies a change to the stashed-peer privacy settings and re-syncs exceptions for every
+ // currently stashed peer when the rules change.
+ let updateStashPrivacy: (@escaping (WinterGramStashPrivacySettings) -> WinterGramStashPrivacySettings) -> Void = { f in
+ let previous = currentWinterGramSettings.stashPrivacy
+ updateSettings { settings in
+ var settings = settings
+ settings.stashPrivacy = f(settings.stashPrivacy)
+ return settings
+ }
+ let updated = f(previous)
+ if updated != previous {
+ for rawPeerId in Set(currentWinterGramSettings.stashedPeerIds) {
+ let peerId = EnginePeer.Id(rawPeerId)
+ let _ = winterGramApplyStashPrivacy(engine: context.engine, peerId: peerId, stashed: true, privacySettings: updated).startStandalone()
+ }
}
- controller.setItemGroups([
- ActionSheetItemGroup(items: items),
- ActionSheetItemGroup(items: [
- ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in
- controller?.dismissAnimated()
- })
- ])
- ])
- presentControllerImpl?(controller, nil)
}
+ // Which inline dropdown (if any) is currently expanded. Atomic mirror for synchronous reads in the
+ // toggle/select closures; promise drives the list rebuild.
+ let expandedDropdownValue = Atomic(value: nil)
+ let expandedDropdownPromise = ValuePromise(nil, ignoreRepeated: true)
+
+ // Whether the Ghost Mode expandable section in the Core menu is open.
+ let ghostExpandedValue = Atomic(value: false)
+ let ghostExpandedPromise = ValuePromise(false, ignoreRepeated: true)
+ let spoofTemplatesEditingValue = Atomic(value: false)
+ let spoofTemplatesEditingPromise = ValuePromise(false, ignoreRepeated: true)
+ let selectedTemplatesValue = Atomic>(value: Set())
+ let selectedTemplatesPromise = ValuePromise>(Set(), ignoreRepeated: true)
+
let arguments = WinterGramSettingsArguments(
+ context: context,
updateSettings: updateSettings,
- presentSendWithoutSound: {
- presentChoice(title: "Send Without Sound", options: [
- ("Never", WinterGramSendWithoutSound.never),
- ("In Ghost Mode", WinterGramSendWithoutSound.inGhostMode),
- ("Always", WinterGramSendWithoutSound.always)
- ], apply: { value, settings in
- var settings = settings
- settings.sendWithoutSound = value
- return settings
- })
+ toggleDropdown: { dropdown in
+ let newValue: WinterGramDropdown? = expandedDropdownValue.with { $0 == dropdown ? nil : dropdown }
+ let _ = expandedDropdownValue.swap(newValue)
+ expandedDropdownPromise.set(newValue)
},
- presentPeerId: {
- presentChoice(title: "Show Peer ID", options: [
- ("Hidden", WinterGramPeerIdDisplay.hidden),
- ("Telegram API", WinterGramPeerIdDisplay.telegramApi),
- ("Bot API", WinterGramPeerIdDisplay.botApi)
- ], apply: { value, settings in
- var settings = settings
- settings.showPeerId = value
- return settings
- })
+ selectDropdownOption: { dropdown, index in
+ let options = winterGramDropdownOptions(dropdown, settings: currentWinterGramSettings)
+ if index >= 0, index < options.count {
+ if dropdown == .stashPrivacy {
+ updateStashPrivacy { privacy in
+ var settings = currentWinterGramSettings
+ settings.stashPrivacy = privacy
+ return options[index].apply(settings).stashPrivacy
+ }
+ } else {
+ updateSettings { options[index].apply($0) }
+ }
+ }
+ // Multi-select dropdowns stay open so the user can toggle more options.
+ if dropdown != .stashPrivacy {
+ let _ = expandedDropdownValue.swap(nil)
+ expandedDropdownPromise.set(nil)
+ }
},
- presentTranslationProvider: {
- presentChoice(title: "Translation Provider", options: [
- ("Telegram", WinterGramTranslationProvider.telegram),
- ("Google", WinterGramTranslationProvider.google),
- ("Yandex", WinterGramTranslationProvider.yandex),
- ("System", WinterGramTranslationProvider.system)
- ], apply: { value, settings in
- var settings = settings
- settings.translationProvider = value
- return settings
- })
+ toggleGhostExpanded: {
+ let newValue = !ghostExpandedValue.with { $0 }
+ let _ = ghostExpandedValue.swap(newValue)
+ ghostExpandedPromise.set(newValue)
},
- presentWebviewPlatform: {
- presentChoice(title: "WebView Platform", options: [
- ("Automatic", WinterGramWebviewPlatform.auto),
- ("iOS", WinterGramWebviewPlatform.ios),
- ("Android", WinterGramWebviewPlatform.android),
- ("macOS", WinterGramWebviewPlatform.macos),
- ("Desktop", WinterGramWebviewPlatform.desktop)
- ], apply: { value, settings in
- var settings = settings
- settings.webviewSpoofPlatform = value
- return settings
- })
+ editSpoofDevice: {
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ let lang = presentationData.strings
+ let current = currentWinterGramSettings.spoofDeviceModel
+
+ let controller = ActionSheetController(presentationData: presentationData)
+ var items: [ActionSheetItem] = []
+ items.append(ActionSheetTextItem(title: lang.WinterGram_SpoofDeviceModel))
+
+ let templates = [
+ ("iPhone 16 Pro Max", "iPhone17,2", "11.1"),
+ ("iPhone 15 Pro", "iPhone16,1", "11.1"),
+ ("iPad Pro M4", "iPad16,3", "11.1"),
+ ("Pixel 9 Pro", "Pixel 9 Pro", "11.1")
+ ]
+
+ for (name, model, version) in templates {
+ items.append(ActionSheetButtonItem(title: name, action: { [weak controller] in
+ controller?.dismissAnimated()
+ updateSettings { var s = $0; s.spoofDeviceModel = model; s.spoofAppVersion = version; return s }
+ }))
+ }
+
+ for preset in currentWinterGramSettings.spoofPresets {
+ items.append(ActionSheetButtonItem(title: preset.name, action: { [weak controller] in
+ controller?.dismissAnimated()
+ updateSettings { var s = $0; s.spoofDeviceModel = preset.deviceModel; s.spoofAppVersion = preset.appVersion; return s }
+ }))
+ }
+
+ items.append(ActionSheetButtonItem(title: lang.WinterGram_Custom, action: { [weak controller] in
+ controller?.dismissAnimated()
+ let prompt = promptController(context: context, text: lang.WinterGram_SpoofDeviceModel, value: current, placeholder: current ?? "iPhone17,2", characterLimit: 64, apply: { value in
+ updateSettings { var s = $0; s.spoofDeviceModel = (value?.isEmpty ?? true) ? nil : value; return s }
+ })
+ presentControllerImpl?(prompt, nil)
+ }))
+
+ items.append(ActionSheetButtonItem(title: lang.WinterGram_SaveCurrentAsProfile, color: .accent, action: { [weak controller] in
+ controller?.dismissAnimated()
+ let prompt = promptController(context: context, text: lang.WinterGram_ProfileName, value: nil, placeholder: "My Profile", characterLimit: 32, apply: { name in
+ if let name = name, !name.isEmpty {
+ updateSettings { var s = $0
+ let preset = WinterGramSpoofPreset(name: name, deviceModel: s.spoofDeviceModel ?? "", appVersion: s.spoofAppVersion ?? "")
+ s.spoofPresets.append(preset)
+ return s
+ }
+ }
+ })
+ presentControllerImpl?(prompt, nil)
+ }))
+
+ items.append(ActionSheetButtonItem(title: lang.WinterGram_DefaultRealDevice, color: .destructive, action: { [weak controller] in
+ controller?.dismissAnimated()
+ updateSettings { var s = $0; s.spoofDeviceModel = nil; s.spoofAppVersion = nil; return s }
+ }))
+
+ controller.setItemGroups([
+ ActionSheetItemGroup(items: items),
+ ActionSheetItemGroup(items: [
+ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in
+ controller?.dismissAnimated()
+ })
+ ])
+ ])
+ presentControllerImpl?(controller, nil)
},
- presentIconPack: {
- presentChoice(title: "Icon Pack", options: [
- ("WinterGram", WinterGramIconPack.wintergram),
- ("AyuGram", WinterGramIconPack.ayugram),
- ("exteraGram", WinterGramIconPack.exteragram),
- ("Telegram", WinterGramIconPack.telegram)
- ], apply: { value, settings in
+ addSpoofTemplate: {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let controller = promptController(context: context, text: lang.WinterGram_ProfileName, value: nil, placeholder: "My Profile", characterLimit: 32, apply: { name in
+ if let name = name?.trimmingCharacters(in: .whitespaces), !name.isEmpty {
+ updateSettings { settings in
+ var settings = settings
+ settings.spoofPresets.append(WinterGramSpoofPreset(name: name, deviceModel: settings.spoofDeviceModel ?? "", appVersion: settings.spoofAppVersion ?? ""))
+ return settings
+ }
+ }
+ })
+ presentControllerImpl?(controller, nil)
+ },
+ toggleSpoofTemplateSelected: { index in
+ let updated = selectedTemplatesValue.modify { selected in
+ var selected = selected
+ if selected.contains(index) {
+ selected.remove(index)
+ } else {
+ selected.insert(index)
+ }
+ return selected
+ }
+ selectedTemplatesPromise.set(updated)
+ },
+ deleteSelectedSpoofTemplates: {
+ let selected = selectedTemplatesValue.with { $0 }
+ guard !selected.isEmpty else { return }
+ updateSettings { settings in
var settings = settings
- settings.iconPack = value
+ let sorted = selected.sorted(by: >)
+ for index in sorted {
+ if index >= 0 && index < settings.spoofPresets.count {
+ settings.spoofPresets.remove(at: index)
+ }
+ }
return settings
+ }
+ let _ = selectedTemplatesValue.swap(Set())
+ selectedTemplatesPromise.set(Set())
+ },
+ editApiId: {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let current = currentWinterGramSettings.customApiId.flatMap { $0 == 0 ? nil : "\($0)" }
+ let controller = promptController(context: context, text: lang.WinterGram_APIID, value: current, placeholder: "1234567", characterLimit: 16, apply: { value in
+ updateSettings { var s = $0
+ if let value = value, let parsed = Int32(value.trimmingCharacters(in: .whitespaces)), parsed != 0 {
+ s.customApiId = parsed
+ } else {
+ s.customApiId = nil
+ }
+ return s
+ }
+ })
+ presentControllerImpl?(controller, nil)
+ },
+ editApiHash: {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let current = currentWinterGramSettings.customApiHash
+ let controller = promptController(context: context, text: lang.WinterGram_APIHash, value: current, placeholder: "0123456789abcdef0123456789abcdef", characterLimit: 64, apply: { value in
+ updateSettings { var s = $0
+ let trimmed = value?.trimmingCharacters(in: .whitespaces)
+ s.customApiHash = (trimmed?.isEmpty ?? true) ? nil : trimmed
+ return s
+ }
+ })
+ presentControllerImpl?(controller, nil)
+ },
+ openStash: {
+ let passcode = currentWinterGramSettings.stashPasscode
+ if passcode.isEmpty {
+ pushControllerImpl?(winterGramStashController(context: context))
+ } else {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let controller = promptController(context: context, text: lang.WinterGram_EnterPasscode, value: nil, placeholder: "••••", characterLimit: 16, apply: { value in
+ if (value ?? "") == passcode {
+ pushControllerImpl?(winterGramStashController(context: context))
+ }
+ })
+ presentControllerImpl?(controller, nil)
+ }
+ },
+ editStashPasscode: {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let current = currentWinterGramSettings.stashPasscode
+ let controller = promptController(context: context, text: lang.WinterGram_StashPasscode, value: current.isEmpty ? nil : current, placeholder: lang.WinterGram_EmptyNoPasscode, characterLimit: 16, apply: { value in
+ updateSettings { var s = $0; s.stashPasscode = (value ?? "").trimmingCharacters(in: .whitespaces); return s }
+ })
+ presentControllerImpl?(controller, nil)
+ },
+ editDeletedMark: {
+ let lang = context.sharedContext.currentPresentationData.with { $0 }.strings
+ let current = currentWinterGramSettings.deletedMark
+ let controller = promptController(context: context, text: lang.WinterGram_DeletedMark, value: current.isEmpty ? nil : current, placeholder: "🧹", characterLimit: 8, apply: { value in
+ updateSettings { var s = $0; s.deletedMark = (value ?? "").trimmingCharacters(in: .whitespaces); return s }
+ })
+ presentControllerImpl?(controller, nil)
+ },
+ openUrl: { url in
+ context.sharedContext.applicationBindings.openUrl(url)
+ },
+ clearDeleted: {
+ let _ = (winterGramClearDeletedMessages(postbox: context.account.postbox)
+ |> deliverOnMainQueue).startStandalone(next: { _ in
+ refreshDeletedCount?()
})
}
)
+ let deletedCountPromise = Promise()
+ deletedCountPromise.set(winterGramDeletedMessagesCount(postbox: context.account.postbox))
+ refreshDeletedCount = {
+ deletedCountPromise.set(winterGramDeletedMessagesCount(postbox: context.account.postbox))
+ }
+
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
- winterGramSettings(accountManager: accountManager)
+ winterGramSettings(accountManager: accountManager),
+ deletedCountPromise.get(),
+ selectedCategoryPromise.get(),
+ expandedDropdownPromise.get(),
+ ghostExpandedPromise.get(),
+ spoofTemplatesEditingPromise.get(),
+ selectedTemplatesPromise.get()
)
|> deliverOnMainQueue
- |> map { presentationData, settings -> (ItemListControllerState, (ItemListNodeState, Any)) in
- let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("WinterGram"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
- let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: winterGramSettingsEntries(settings: settings), style: .blocks, animateChanges: true)
+ |> map { presentationData, settings, deletedCount, selectedCategory, expandedDropdown, ghostExpanded, spoofTemplatesEditing, selectedTemplates -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ let lang = presentationData.strings
+ let controllerState: ItemListControllerState
+ if let category = category {
+ let controllerStateTitle = wntOption(category.title, presentationData.strings)
+ let rightNavigationButton: ItemListNavigationButton?
+ if category == .spoofing, !settings.spoofPresets.isEmpty {
+ rightNavigationButton = ItemListNavigationButton(content: .text(spoofTemplatesEditing ? presentationData.strings.Common_Done : presentationData.strings.Common_Edit), style: spoofTemplatesEditing ? .bold : .regular, enabled: true, action: {
+ let nextValue = !spoofTemplatesEditingValue.with { $0 }
+ let _ = spoofTemplatesEditingValue.swap(nextValue)
+ if !nextValue {
+ let _ = selectedTemplatesValue.swap(Set())
+ selectedTemplatesPromise.set(Set())
+ }
+ spoofTemplatesEditingPromise.set(nextValue)
+ })
+ } else {
+ rightNavigationButton = nil
+ }
+ controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(controllerStateTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
+ } else {
+ let titles = sectionTabs.map { wntOption($0.title, lang) }
+ let selectedIndex = sectionTabs.firstIndex(of: selectedCategory) ?? 0
+ controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl(titles, selectedIndex), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
+ }
+ let activeCategory = category ?? selectedCategory
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: winterGramSettingsEntries(presentationData: presentationData, settings: settings, deletedCount: deletedCount, expandedDropdown: expandedDropdown, ghostExpanded: ghostExpanded, spoofTemplatesEditing: spoofTemplatesEditing, selectedTemplates: selectedTemplates, category: activeCategory), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
+ controller.titleControlValueChanged = { index in
+ selectedCategoryPromise.set(sectionTabs[index])
+ }
+
+ var bannerController: UndoOverlayController?
+ let restartBannerDisposable = (requiresRestartPromise.get()
+ |> deliverOnMainQueue).start(next: { [weak controller] needsRestart in
+ guard let controller = controller else { return }
+ if needsRestart {
+ if bannerController == nil {
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ let banner = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.WinterGram_RestartRequired, text: presentationData.strings.WinterGram_SomeSettingsWillTakeEffectAfterRestart, timeout: nil, customUndoText: presentationData.strings.WinterGram_Restart), elevatedLayout: false, action: { action in
+ if action == .undo {
+ exit(0)
+ }
+ return true
+ })
+ bannerController = banner
+ controller.present(banner, in: .window(.root))
+ }
+ } else {
+ bannerController?.dismiss()
+ bannerController = nil
+ }
+ })
+
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
+ pushControllerImpl = { [weak controller] c in
+ // Keep the restart-banner subscription alive for the controller's lifetime: this
+ // closure is retained by `arguments`, which the ItemListController holds via its state.
+ let _ = restartBannerDisposable
+ (controller?.navigationController as? NavigationController)?.pushViewController(c)
+ }
return controller
}
diff --git a/submodules/SettingsUI/Sources/WinterGramStashController.swift b/submodules/SettingsUI/Sources/WinterGramStashController.swift
new file mode 100644
index 0000000000..c25af8528d
--- /dev/null
+++ b/submodules/SettingsUI/Sources/WinterGramStashController.swift
@@ -0,0 +1,193 @@
+import Foundation
+import UIKit
+import Display
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import TelegramUIPreferences
+import ItemListUI
+import ItemListPeerItem
+import PresentationDataUtils
+import AccountContext
+
+private final class WinterGramStashArguments {
+ let context: AccountContext
+ let openPeer: (EnginePeer) -> Void
+ let removePeer: (EnginePeer.Id) -> Void
+ let updateRevealedPeerId: (EnginePeer.Id?) -> Void
+
+ init(
+ context: AccountContext,
+ openPeer: @escaping (EnginePeer) -> Void,
+ removePeer: @escaping (EnginePeer.Id) -> Void,
+ updateRevealedPeerId: @escaping (EnginePeer.Id?) -> Void
+ ) {
+ self.context = context
+ self.openPeer = openPeer
+ self.removePeer = removePeer
+ self.updateRevealedPeerId = updateRevealedPeerId
+ }
+}
+
+private enum WinterGramStashSection: Int32 {
+ case peers
+}
+
+private enum WinterGramStashEntry: ItemListNodeEntry {
+ case header
+ case peer(Int32, EnginePeer, Bool)
+ case empty
+
+ var section: ItemListSectionId {
+ return WinterGramStashSection.peers.rawValue
+ }
+
+ var stableId: Int64 {
+ switch self {
+ case .header:
+ return 0
+ case let .peer(_, peer, _):
+ return peer.id.id._internalGetInt64Value()
+ case .empty:
+ return 1
+ }
+ }
+
+ static func ==(lhs: WinterGramStashEntry, rhs: WinterGramStashEntry) -> Bool {
+ switch lhs {
+ case .header:
+ if case .header = rhs {
+ return true
+ }
+ return false
+ case let .peer(lhsIndex, lhsPeer, lhsRevealed):
+ if case let .peer(rhsIndex, rhsPeer, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsRevealed == rhsRevealed {
+ return true
+ }
+ return false
+ case .empty:
+ if case .empty = rhs {
+ return true
+ }
+ return false
+ }
+ }
+
+ static func <(lhs: WinterGramStashEntry, rhs: WinterGramStashEntry) -> Bool {
+ return lhs.sortIndex < rhs.sortIndex
+ }
+
+ private var sortIndex: Int32 {
+ switch self {
+ case .header:
+ return 0
+ case let .peer(index, _, _):
+ return 10 + index
+ case .empty:
+ return 1
+ }
+ }
+
+ func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
+ let arguments = arguments as! WinterGramStashArguments
+ switch self {
+ case .header:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.WinterGram_HiddenArchiveInfo), sectionId: self.section)
+ case let .peer(_, peer, revealed):
+ let chatPresentationData = arguments.context.sharedContext.currentPresentationData.with { $0 }
+ return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: chatPresentationData.dateTimeFormat, nameDisplayOrder: chatPresentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
+ arguments.openPeer(peer)
+ }, setPeerIdWithRevealedOptions: { peerId, _ in
+ arguments.updateRevealedPeerId(peerId)
+ }, removePeer: { peerId in
+ arguments.removePeer(peerId)
+ })
+ case .empty:
+ return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.WinterGram_HiddenArchiveEmpty), sectionId: self.section)
+ }
+ }
+}
+
+public func winterGramStashController(context: AccountContext) -> ViewController {
+ let revealedPeerId = ValuePromise(nil)
+ let revealedPeerIdValue = Atomic(value: nil)
+
+ let accountManager = context.sharedContext.accountManager
+
+ let arguments = WinterGramStashArguments(
+ context: context,
+ openPeer: { peer in
+ if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController {
+ context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
+ }
+ },
+ removePeer: { peerId in
+ let rawId = peerId.toInt64()
+ let _ = updateWinterGramSettingsInteractively(accountManager: accountManager, { settings in
+ var settings = settings
+ settings.stashedPeerIds.removeAll(where: { $0 == rawId })
+ return settings
+ }).start()
+ // Remove the peer from the server-side privacy exceptions added when it was stashed.
+ let _ = winterGramApplyStashPrivacy(engine: context.engine, peerId: peerId, stashed: false, privacySettings: currentWinterGramSettings.stashPrivacy).startStandalone()
+ },
+ updateRevealedPeerId: { peerId in
+ let _ = revealedPeerIdValue.swap(peerId)
+ revealedPeerId.set(peerId)
+ }
+ )
+
+ let peers = winterGramSettings(accountManager: accountManager)
+ |> map { settings -> [EnginePeer.Id] in
+ var seen = Set()
+ var result: [EnginePeer.Id] = []
+ for rawPeerId in settings.stashedPeerIds {
+ if seen.insert(rawPeerId).inserted {
+ result.append(EnginePeer.Id(rawPeerId))
+ }
+ }
+ return result
+ }
+ |> distinctUntilChanged
+ |> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
+ return context.engine.data.subscribe(
+ EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
+ )
+ |> map { peerMap -> [EnginePeer] in
+ var result: [EnginePeer] = []
+ for id in peerIds {
+ if let maybePeer = peerMap[id], let peer = maybePeer {
+ result.append(peer)
+ }
+ }
+ return result
+ }
+ }
+
+ let signal = combineLatest(
+ context.sharedContext.presentationData,
+ peers,
+ revealedPeerId.get()
+ )
+ |> map { presentationData, peers, revealedPeerId -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ var entries: [WinterGramStashEntry] = []
+ entries.append(.header)
+ if peers.isEmpty {
+ entries.append(.empty)
+ } else {
+ var index: Int32 = 0
+ for peer in peers {
+ entries.append(.peer(index, peer, revealedPeerId == peer.id))
+ index += 1
+ }
+ }
+
+ let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WinterGram_HiddenArchive), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks)
+
+ return (controllerState, (listState, arguments))
+ }
+
+ let controller = ItemListController(context: context, state: signal)
+ return controller
+}
diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift
index 24f9d915ae..2037a94031 100644
--- a/submodules/TelegramCore/Sources/Account/AccountManager.swift
+++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift
@@ -217,6 +217,9 @@ private var declaredEncodables: Void = {
declareEncodable(AuthSessionInfoAttribute.self, f: { AuthSessionInfoAttribute(decoder: $0) })
declareEncodable(TranslationMessageAttribute.self, f: { TranslationMessageAttribute(decoder: $0) })
declareEncodable(TranslationMessageAttribute.Additional.self, f: { TranslationMessageAttribute.Additional(decoder: $0) })
+ declareEncodable(WinterGramEditHistoryAttribute.self, f: { WinterGramEditHistoryAttribute(decoder: $0) })
+ declareEncodable(WinterGramEditHistoryAttribute.Revision.self, f: { WinterGramEditHistoryAttribute.Revision(decoder: $0) })
+ declareEncodable(WinterGramDeletedMessageAttribute.self, f: { WinterGramDeletedMessageAttribute(decoder: $0) })
declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) })
declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) })
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
@@ -277,7 +280,7 @@ public func performAppGroupUpgrades(appGroupPath: String, rootPath: String) {
}
}
}
-
+
do {
// WinterGram: include account data in the device's iCloud/iTunes backup so accounts
// survive a restore. Tradeoff: session auth keys are then part of the iCloud backup.
@@ -343,10 +346,10 @@ public func currentAccount(allocateIfNotExists: Bool, networkArguments: NetworkI
}
}
|> distinctUntilChanged
-
+
return Signal { subscriber in
subscriber.putNext(accountResult)
-
+
return updatedKind.start(next: { value in
if value {
reload.set(true)
@@ -413,19 +416,19 @@ public func managedCleanupAccounts(networkArguments: NetworkInitializationArgume
}
}
}
-
+
var disposables = disposables
-
+
for id in disposables.keys {
if validIds[id] == nil {
disposeList.append((id, disposables[id]!))
}
}
-
+
for (id, _) in disposeList {
disposables.removeValue(forKey: id)
}
-
+
for (id, attributes) in validIds {
if disposables[id] == nil {
let disposable = MetaDisposable()
@@ -433,7 +436,7 @@ public func managedCleanupAccounts(networkArguments: NetworkInitializationArgume
disposables[id] = disposable
}
}
-
+
return disposables
}
for (_, disposable) in disposeList {
@@ -443,7 +446,7 @@ public func managedCleanupAccounts(networkArguments: NetworkInitializationArgume
Logger.shared.log("managedCleanupAccounts", "cleanup \(id), current is \(String(describing: view.currentRecord?.id))")
disposable.set(cleanupAccount(networkArguments: networkArguments, accountManager: accountManager, id: id, encryptionParameters: encryptionParameters, attributes: attributes, rootPath: rootPath, auxiliaryMethods: auxiliaryMethods).start())
}
-
+
var validPaths = Set()
for record in view.records {
if let temporarySessionId = record.temporarySessionId, temporarySessionId != currentTemporarySessionId {
@@ -454,7 +457,7 @@ public func managedCleanupAccounts(networkArguments: NetworkInitializationArgume
if let record = view.currentAuthAccount {
validPaths.insert("\(accountRecordIdPathName(record.id))")
}
-
+
DispatchQueue.global(qos: .utility).async {
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: [], options: []) {
for url in files {
@@ -467,7 +470,7 @@ public func managedCleanupAccounts(networkArguments: NetworkInitializationArgume
}
}
})
-
+
return ActionDisposable {
disposable.dispose()
}
diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
index d80793f3b2..840a50e907 100644
--- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
+++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
@@ -8,7 +8,7 @@ import EncryptionProvider
private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribute?, _ updatedReactions: ReactionsMessageAttribute?, message: Message, transaction: Transaction) -> (reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)? {
if let updatedReactions = updatedReactions, !message.flags.contains(.Incoming), message.id.peerId.namespace == Namespaces.Peer.CloudUser {
let prev = previousReactions?.reactions ?? []
-
+
let updated = updatedReactions.reactions.filter { value in
return !prev.contains(where: {
$0.value == value.value && $0.count == value.count
@@ -20,18 +20,18 @@ private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribu
let myPrevious = prev.filter { value in
return value.chosenOrder != nil
}.first
-
+
let previousCount = prev.reduce(0, {
$0 + $1.count
})
let updatedCount = updatedReactions.reactions.reduce(0, {
$0 + $1.count
})
-
+
let newReaction = updated.filter {
$0.chosenOrder == nil
}.first?.value
-
+
if !updated.isEmpty && myUpdated == myPrevious, updatedCount >= previousCount, let value = newReaction {
if let reactionAuthor = transaction.getPeer(message.id.peerId) {
return (reactionAuthor: reactionAuthor, reaction: value, message: message, timestamp: Int32(Date().timeIntervalSince1970))
@@ -44,7 +44,7 @@ private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribu
private func peerIdsFromUpdateGroups(_ groups: [UpdateGroup]) -> Set {
var peerIds = Set()
-
+
for group in groups {
for update in group.updates {
for peerId in update.peerIds {
@@ -64,13 +64,13 @@ private func peerIdsFromUpdateGroups(_ groups: [UpdateGroup]) -> Set {
break
}
}
-
+
return peerIds
}
private func activeChannelsFromUpdateGroups(_ groups: [UpdateGroup]) -> Set {
var peerIds = Set