mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' into experimental-2
This commit is contained in:
commit
1631103633
171
Makefile
171
Makefile
@ -1,171 +0,0 @@
|
||||
.PHONY : kill_xcode clean bazel_app_debug_arm64 bazel_app_debug_sim_arm64 bazel_app_arm64 bazel_app_armv7 bazel_app check_sandbox_debug_build bazel_project bazel_project_noextensions
|
||||
|
||||
APP_VERSION="7.3"
|
||||
CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
|
||||
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)
|
||||
|
||||
BAZEL=$(shell which bazel)
|
||||
|
||||
ifneq ($(BAZEL_HTTP_CACHE_URL),)
|
||||
export BAZEL_CACHE_FLAGS=\
|
||||
--remote_cache="$(BAZEL_HTTP_CACHE_URL)" --experimental_remote_downloader="$(BAZEL_HTTP_CACHE_URL)"
|
||||
else ifneq ($(BAZEL_CACHE_DIR),)
|
||||
export BAZEL_CACHE_FLAGS=\
|
||||
--disk_cache="${BAZEL_CACHE_DIR}"
|
||||
endif
|
||||
|
||||
ifneq ($(BAZEL_KEEP_GOING),)
|
||||
export BAZEL_KEEP_GOING_FLAGS=\
|
||||
-k
|
||||
else ifneq ($(BAZEL_CACHE_DIR),)
|
||||
export BAZEL_KEEP_GOING_FLAGS=
|
||||
endif
|
||||
|
||||
BAZEL_COMMON_FLAGS=\
|
||||
--announce_rc \
|
||||
--features=swift.use_global_module_cache \
|
||||
--features=swift.split_derived_files_generation \
|
||||
--features=swift.skip_function_bodies_for_derived_files \
|
||||
--jobs=${CORE_COUNT} \
|
||||
${BAZEL_KEEP_GOING_FLAGS} \
|
||||
|
||||
BAZEL_DEBUG_FLAGS=\
|
||||
--features=swift.enable_batch_mode \
|
||||
--swiftcopt=-j${CORE_COUNT_MINUS_ONE} \
|
||||
--experimental_guard_against_concurrent_changes \
|
||||
|
||||
BAZEL_SANDBOX_FLAGS=\
|
||||
--strategy=Genrule=sandboxed \
|
||||
--spawn_strategy=sandboxed \
|
||||
--strategy=SwiftCompile=sandboxed \
|
||||
|
||||
# --num-threads 0 forces swiftc to generate one object file per module; it:
|
||||
# 1. resolves issues with the linker caused by swift-objc mixing.
|
||||
# 2. makes the resulting binaries significantly smaller (up to 9% for this project).
|
||||
BAZEL_OPT_FLAGS=\
|
||||
--features=swift.opt_uses_wmo \
|
||||
--features=swift.opt_uses_osize \
|
||||
--swiftcopt='-num-threads' --swiftcopt='0' \
|
||||
--features=dead_strip \
|
||||
--objc_enable_binary_stripping \
|
||||
--apple_bitcode=watchos=embedded \
|
||||
|
||||
kill_xcode:
|
||||
killall Xcode || true
|
||||
|
||||
clean:
|
||||
"${BAZEL}" clean --expunge
|
||||
|
||||
bazel_app_debug_arm64:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} \
|
||||
-c dbg \
|
||||
--ios_multi_cpus=arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--verbose_failures
|
||||
|
||||
bazel_webrtc:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build third-party/webrtc:webrtc_lib ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} ${BAZEL_SANDBOX_FLAGS} \
|
||||
-c dbg \
|
||||
--ios_multi_cpus=arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--verbose_failures
|
||||
|
||||
bazel_app_debug_sim_arm64:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} \
|
||||
-c dbg \
|
||||
--ios_multi_cpus=sim_arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--verbose_failures
|
||||
|
||||
bazel_app_arm64:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_OPT_FLAGS} \
|
||||
-c opt \
|
||||
--ios_multi_cpus=arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--apple_generate_dsym \
|
||||
--output_groups=+dsyms \
|
||||
--verbose_failures
|
||||
|
||||
bazel_app_armv7:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_OPT_FLAGS} \
|
||||
-c opt \
|
||||
--ios_multi_cpus=armv7 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--apple_generate_dsym \
|
||||
--output_groups=+dsyms \
|
||||
--verbose_failures
|
||||
|
||||
bazel_app:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_OPT_FLAGS} \
|
||||
-c opt \
|
||||
--ios_multi_cpus=armv7,arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--apple_generate_dsym \
|
||||
--output_groups=+dsyms \
|
||||
--verbose_failures
|
||||
|
||||
check_sandbox_debug_build:
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram distribution
|
||||
"${BAZEL}" build Telegram/Telegram ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} \
|
||||
-c opt \
|
||||
--ios_multi_cpus=arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--apple_generate_dsym \
|
||||
--output_groups=+dsyms \
|
||||
--verbose_failures
|
||||
|
||||
bazel_project: kill_xcode
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="0" \
|
||||
build-system/prepare-build.sh Telegram development
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
build-system/generate-xcode-project.sh Telegram
|
||||
|
||||
bazel_project_noextensions: kill_xcode
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
TELEGRAM_DISABLE_EXTENSIONS="1" \
|
||||
build-system/prepare-build.sh Telegram development
|
||||
APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
BAZEL_HTTP_CACHE_URL="${BAZEL_HTTP_CACHE_URL}" \
|
||||
build-system/generate-xcode-project.sh Telegram
|
114
README.md
114
README.md
@ -1,16 +1,108 @@
|
||||
# Telegram iOS Source Code Compilation Guide
|
||||
|
||||
1. Install the brew package manager, if you haven’t already.
|
||||
2. Install the packages yasm, cmake:
|
||||
```
|
||||
brew install yasm cmake
|
||||
```
|
||||
3. Clone the project from GitHub:
|
||||
We welcome all developers to use our API and source code to create applications on our platform.
|
||||
There are several things we require from **all developers** for the moment.
|
||||
|
||||
# Creating your Telegram Application
|
||||
|
||||
1. [**Obtain your own api_id**](https://core.telegram.org/api/obtaining_api_id) for your application.
|
||||
2. Please **do not** use the name Telegram for your app — or make sure your users understand that it is unofficial.
|
||||
3. Kindly **do not** use our standard logo (white paper plane in a blue circle) as your app's logo.
|
||||
3. Please study our [**security guidelines**](https://core.telegram.org/mtproto/security_guidelines) and take good care of your users' data and privacy.
|
||||
4. Please remember to publish **your** code too in order to comply with the licences.
|
||||
|
||||
# Compilation Guide
|
||||
|
||||
1. Install Xcode (directly from https://developer.apple.com/download/more or using the App Store).
|
||||
2. Clone the project from GitHub:
|
||||
|
||||
```
|
||||
git clone --recursive https://github.com/TelegramMessenger/Telegram-iOS.git
|
||||
git clone --recursive -j8 https://github.com/TelegramMessenger/Telegram-iOS.git
|
||||
```
|
||||
|
||||
3. Download Bazel 3.7.0
|
||||
|
||||
```
|
||||
mkdir -p $HOME/bazel-dist
|
||||
cd $HOME/bazel-dist
|
||||
curl -O -L https://github.com/bazelbuild/bazel/releases/download/3.7.0/bazel-3.7.0-darwin-x86_64
|
||||
mv bazel-3.7.0* bazel
|
||||
```
|
||||
|
||||
Verify that it's working
|
||||
|
||||
```
|
||||
chmod +x bazel
|
||||
./bazel --version
|
||||
```
|
||||
|
||||
4. Adjust configuration parameters
|
||||
|
||||
```
|
||||
mkdir -p $HOME/telegram-configuration
|
||||
cp -R build-system/example-configuration/* $HOME/telegram-configuration/
|
||||
```
|
||||
|
||||
- Modify the values in `variables.bzl`
|
||||
- Replace the provisioning profiles in `provisioning` with valid files
|
||||
|
||||
5. (Optional) Create a build cache directory to speed up rebuilds
|
||||
|
||||
```
|
||||
mkdir -p "$HOME/telegram-bazel-cache"
|
||||
```
|
||||
|
||||
5. Build the app
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--bazel="$HOME/bazel-dist/bazel" \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
build \
|
||||
--configurationPath="$HOME/telegram-configuration" \
|
||||
--buildNumber=100001 \
|
||||
--configuration=release_universal
|
||||
```
|
||||
|
||||
6. (Optional) Generate an Xcode project
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--bazel="$HOME/bazel-dist/bazel" \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath="$HOME/telegram-configuration" \
|
||||
--disableExtensions
|
||||
```
|
||||
|
||||
It is possible to generate a project that does not require any codesigning certificates to be installed: add `--disableProvisioningProfiles` flag:
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--bazel="$HOME/bazel-dist/bazel" \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath="$HOME/telegram-configuration" \
|
||||
--disableExtensions \
|
||||
--disableProvisioningProfiles
|
||||
```
|
||||
|
||||
|
||||
Tip: use `--disableExtensions` when developing to speed up development by not building application extensions and the WatchOS app.
|
||||
|
||||
|
||||
# Tips
|
||||
|
||||
Bazel is used to build the app. To simplify the development setup a helper script is provided (`build-system/Make/Make.py`). See help:
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py --help
|
||||
python3 build-system/Make/Make.py build --help
|
||||
python3 build-system/Make/Make.py generateProject --help
|
||||
```
|
||||
|
||||
Each release is built using specific Xcode and Bazel versions (see `versions.json`). The helper script checks the versions of installed software and reports an error if they don't match the ones specified in `versions.json`. There are flags that allow to bypass these checks:
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py --overrideBazelVersion build ... # Don't check the version of Bazel
|
||||
python3 build-system/Make/Make.py --overrideXcodeVersion build ... # Don't check the version of Xcode
|
||||
```
|
||||
4. Open Telegram-iOS.workspace.
|
||||
5. Open the Telegram-iOS-Fork scheme.
|
||||
6. Start the compilation process.
|
||||
7. To run the app on your device, you will need to set the correct values for the signature, .entitlements files and package IDs in accordance with your developer account values.
|
||||
|
245
Telegram/BUILD
245
Telegram/BUILD
@ -1,3 +1,7 @@
|
||||
load("@bazel_skylib//rules:common_settings.bzl",
|
||||
"bool_flag",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:ios.bzl",
|
||||
"ios_application",
|
||||
"ios_extension",
|
||||
@ -18,13 +22,10 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
)
|
||||
|
||||
load(
|
||||
"//build-input/data:variables.bzl",
|
||||
"telegram_build_number",
|
||||
"telegram_version",
|
||||
"@build_configuration//:variables.bzl",
|
||||
"telegram_bundle_id",
|
||||
"telegram_aps_environment",
|
||||
"telegram_team_id",
|
||||
"telegram_disable_extensions",
|
||||
)
|
||||
|
||||
config_setting(
|
||||
@ -34,6 +35,32 @@ config_setting(
|
||||
},
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "disableExtensions",
|
||||
build_setting_default = False,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "disableProvisioningProfiles",
|
||||
build_setting_default = False,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "disableExtensionsSetting",
|
||||
flag_values = {
|
||||
":disableExtensions": "True",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "disableProvisioningProfilesSetting",
|
||||
flag_values = {
|
||||
":disableProvisioningProfiles": "True",
|
||||
},
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "empty",
|
||||
outs = ["empty.swift"],
|
||||
@ -190,14 +217,20 @@ swift_library(
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "AdditionalInfoPlist",
|
||||
name = "BuildNumberInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<string>{buildNumber}</string>
|
||||
"""
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "UrlTypesInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@ -210,16 +243,6 @@ plist_fragment(
|
||||
<string>telegram</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>{telegram_bundle_id}.ton</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ton</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
@ -232,8 +255,6 @@ plist_fragment(
|
||||
</dict>
|
||||
</array>
|
||||
""".format(
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
)
|
||||
)
|
||||
@ -269,6 +290,12 @@ official_unrestricted_voip_fragment = """
|
||||
"""
|
||||
unrestricted_voip_fragment = official_unrestricted_voip_fragment if telegram_bundle_id in official_bundle_ids else ""
|
||||
|
||||
official_carplay_fragment = """
|
||||
<key>com.apple.developer.carplay-messaging</key>
|
||||
<true/>
|
||||
"""
|
||||
carplay_fragment = official_carplay_fragment if telegram_bundle_id in official_bundle_ids else ""
|
||||
|
||||
telegram_entitlements_template = """
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
@ -296,7 +323,7 @@ telegram_entitlements_template = """
|
||||
<string>{telegram_team_id}.{telegram_bundle_id}</string>
|
||||
<key>com.apple.developer.icloud-container-environment</key>
|
||||
<string>{telegram_icloud_environment}</string>
|
||||
""" + apple_pay_merchants_fragment + unrestricted_voip_fragment
|
||||
""" + apple_pay_merchants_fragment + unrestricted_voip_fragment + carplay_fragment
|
||||
|
||||
plist_fragment(
|
||||
name = "TelegramEntitlements",
|
||||
@ -371,13 +398,8 @@ plist_fragment(
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
""".format(
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
<string>{telegramVersion}</string>
|
||||
"""
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
@ -478,11 +500,15 @@ watchos_extension(
|
||||
infoplists = [
|
||||
":WatchExtensionInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
":WatchExtensionNSExtensionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "5.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:WatchExtension.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:WatchExtension.mobileprovision",
|
||||
}),
|
||||
resources = [
|
||||
":TelegramWatchExtensionResources",
|
||||
],
|
||||
@ -505,11 +531,15 @@ watchos_application(
|
||||
infoplists = [
|
||||
":WatchAppInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
"BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
":WatchAppCompanionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "5.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:WatchApp.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:WatchApp.mobileprovision",
|
||||
}),
|
||||
resources = [
|
||||
":TelegramWatchAppResources",
|
||||
":TelegramWatchAppAssets",
|
||||
@ -528,20 +558,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.MtProtoKit</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>MtProtoKit</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -556,6 +580,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":MtProtoKitInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -571,20 +597,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.SwiftSignalKit</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SwiftSignalKit</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -599,6 +619,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":SwiftSignalKitInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -614,20 +636,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.Postbox</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Postbox</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -642,6 +658,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":PostboxInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
@ -660,20 +678,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.TelegramApi</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TelegramApi</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -688,6 +700,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":TelegramApiInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -703,20 +717,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.SyncCore</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SyncCore</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -731,6 +739,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":SyncCoreInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
@ -750,20 +760,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.TelegramCore</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TelegramCore</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -778,13 +782,14 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":TelegramCoreInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
frameworks = [
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":SyncCoreFramework",
|
||||
#":TelegramApiFramework",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -800,20 +805,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.AsyncDisplayKit</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>AsyncDisplayKit</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -828,6 +827,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":AsyncDisplayKitInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -843,20 +844,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.Display</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Display</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -914,6 +909,8 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":DisplayInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
@ -933,20 +930,14 @@ plist_fragment(
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{telegram_bundle_id}.TelegramUI</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{telegram_build_number}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TelegramUI</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{telegram_version}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
""".format(
|
||||
telegram_bundle_id = telegram_bundle_id,
|
||||
telegram_version = telegram_version,
|
||||
telegram_build_number = telegram_build_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -961,12 +952,13 @@ ios_framework(
|
||||
],
|
||||
infoplists = [
|
||||
":TelegramUIInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
frameworks = [
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
#":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
":TelegramCoreFramework",
|
||||
":AsyncDisplayKitFramework",
|
||||
@ -1052,10 +1044,14 @@ ios_extension(
|
||||
infoplists = [
|
||||
":ShareInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:Share.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:Share.mobileprovision",
|
||||
}),
|
||||
deps = [":ShareExtensionLib"],
|
||||
frameworks = [
|
||||
":TelegramUIFramework"
|
||||
@ -1120,10 +1116,14 @@ ios_extension(
|
||||
infoplists = [
|
||||
":NotificationContentInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
],
|
||||
minimum_os_version = "10.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:NotificationContent.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:NotificationContent.mobileprovision",
|
||||
}),
|
||||
deps = [":NotificationContentExtensionLib"],
|
||||
frameworks = [
|
||||
":TelegramUIFramework"
|
||||
@ -1157,25 +1157,35 @@ plist_fragment(
|
||||
)
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "GeneratedSources",
|
||||
module_name = "GeneratedSources",
|
||||
srcs = glob([
|
||||
"Generated/**/*.swift",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "WidgetExtensionLib",
|
||||
module_name = "WidgetExtensionLib",
|
||||
srcs = glob([
|
||||
"WidgetKitWidget/**/*.swift",
|
||||
"Generated/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
"SiriIntents/Intents.intentdefinition",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/WidgetItems:WidgetItems",
|
||||
"//submodules/WidgetItems:WidgetItems_iOS14",
|
||||
"//submodules/WidgetItemsUtils:WidgetItemsUtils",
|
||||
"//submodules/AppLockState:AppLockState",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
|
||||
":GeneratedSources",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1191,11 +1201,15 @@ ios_extension(
|
||||
infoplists = [
|
||||
":WidgetInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
],
|
||||
minimum_os_version = "14.0",
|
||||
provides_main = True,
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:Widget.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:Widget.mobileprovision",
|
||||
}),
|
||||
deps = [":WidgetExtensionLib"],
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
@ -1251,7 +1265,6 @@ swift_library(
|
||||
module_name = "IntentsExtensionLib",
|
||||
srcs = glob([
|
||||
"SiriIntents/**/*.swift",
|
||||
"Generated/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
"SiriIntents/Intents.intentdefinition",
|
||||
@ -1265,6 +1278,7 @@ swift_library(
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
|
||||
"//submodules/AppLockState:AppLockState",
|
||||
":GeneratedSources",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1280,16 +1294,19 @@ ios_extension(
|
||||
infoplists = [
|
||||
":IntentsInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
],
|
||||
minimum_os_version = "10.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:Intents.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:Intents.mobileprovision",
|
||||
}),
|
||||
deps = [":IntentsExtensionLib"],
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramCoreFramework",
|
||||
#":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
],
|
||||
)
|
||||
@ -1331,17 +1348,19 @@ ios_extension(
|
||||
infoplists = [
|
||||
":NotificationServiceInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":AppNameInfoPlist",
|
||||
],
|
||||
minimum_os_version = "10.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:NotificationService.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:NotificationService.mobileprovision",
|
||||
}),
|
||||
deps = ["//Telegram/NotificationService:NotificationServiceExtensionLib"],
|
||||
frameworks = [
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
#":TelegramApiFramework",
|
||||
#":SyncCoreFramework",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1522,11 +1541,16 @@ ios_application(
|
||||
),
|
||||
families = ["iphone", "ipad"],
|
||||
minimum_os_version = "9.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:Telegram.mobileprovision",
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:Telegram.mobileprovision",
|
||||
}),
|
||||
entitlements = ":TelegramEntitlements.entitlements",
|
||||
infoplists = [
|
||||
":TelegramInfoPlist",
|
||||
":AdditionalInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
":UrlTypesInfoPlist",
|
||||
],
|
||||
ipa_post_processor = ":AddAlternateIcons",
|
||||
resources = [
|
||||
@ -1547,15 +1571,20 @@ ios_application(
|
||||
strings = [
|
||||
":AppStringResources",
|
||||
],
|
||||
extensions = [
|
||||
] if telegram_disable_extensions else [
|
||||
":ShareExtension",
|
||||
":NotificationContentExtension",
|
||||
":NotificationServiceExtension",
|
||||
":IntentsExtension",
|
||||
":WidgetExtension",
|
||||
],
|
||||
watch_application = ":TelegramWatchApp",
|
||||
extensions = select({
|
||||
":disableExtensionsSetting": [],
|
||||
"//conditions:default": [
|
||||
":ShareExtension",
|
||||
":NotificationContentExtension",
|
||||
":NotificationServiceExtension",
|
||||
":IntentsExtension",
|
||||
# ":WidgetExtension",
|
||||
],
|
||||
}),
|
||||
watch_application = select({
|
||||
":disableExtensionsSetting": None,
|
||||
"//conditions:default": ":TelegramWatchApp",
|
||||
}),
|
||||
deps = [
|
||||
":Main",
|
||||
":Lib",
|
||||
|
@ -9,6 +9,7 @@ import Contacts
|
||||
import OpenSSLEncryptionProvider
|
||||
import AppLockState
|
||||
import UIKit
|
||||
import GeneratedSources
|
||||
|
||||
private var accountCache: Account?
|
||||
|
||||
|
BIN
Telegram/Telegram-iOS/Resources/Invite.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Invite.tgs
Normal file
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/PlaneLogo.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/PlaneLogo.tgs
Normal file
Binary file not shown.
17
Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist
Normal file
17
Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IntentPhrases</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IntentName</key>
|
||||
<string>INSendMessageIntent</string>
|
||||
<key>IntentExamples</key>
|
||||
<array>
|
||||
<string>Send a Telegram message to Alex saying I'll be there in 10 minutes</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
12
Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
Normal file
12
Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
Normal file
@ -0,0 +1,12 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"NSContactsUsageDescription" = "Telegram будзе запампоўваць вашы кантакты на свае моцна абароненыя і зашыфраваныя воблачныя серверы, каб вы маглі кантактаваць са сваімі сябрамі з любой вашай прылады.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Калі вы адпраўляе месцазнаходжанне сваім сябрам, Telegram патрэбны доступ да сэрвісаў геалакацыі, каб размясціць вас на карце.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі.";
|
||||
"NSLocationAlwaysUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі. Гэта таксама неабходна для таго, каб адпраўляць месцазнаходжанне праз Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі рабіць і адпраўляць фота і відэа, а таксама відэавыклікі.";
|
||||
"NSPhotoLibraryUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі абагульваць фота і відэа са сваёй галерэі.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі захоўваць фота і відэа ў сваю галерэю.";
|
||||
"NSMicrophoneUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі запісваць і абагульваць галасавыя паведамленні і відэа з гукам.";
|
||||
"NSSiriUsageDescription" = "Вы можаце адпраўляць паведамленні праз Siri.";
|
||||
"NSFaceIDUsageDescription" = "Вы можаце карыстацца Face ID для разблакіравання праграмы.";
|
@ -522,6 +522,7 @@
|
||||
"Notification.Kicked" = "%@ removed %@";
|
||||
"Notification.CreatedChat" = "%@ created a group";
|
||||
"Notification.CreatedChannel" = "Channel created";
|
||||
"Notification.CreatedGroup" = "Group created";
|
||||
"Notification.CreatedChatWithTitle" = "%@ created the group \"%@\" ";
|
||||
"Notification.Joined" = "%@ joined Telegram";
|
||||
"Notification.ChangedGroupName" = "%@ changed group name to \"%@\" ";
|
||||
@ -568,6 +569,7 @@
|
||||
|
||||
"ConversationProfile.LeaveDeleteAndExit" = "Delete and Exit";
|
||||
"Group.LeaveGroup" = "Leave Group";
|
||||
"Group.DeleteGroup" = "Delete Group";
|
||||
|
||||
"Conversation.Megabytes" = "%.1f MB";
|
||||
"Conversation.Kilobytes" = "%d KB";
|
||||
@ -1129,6 +1131,7 @@
|
||||
"ShareFileTip.CloseTip" = "Close Tip";
|
||||
|
||||
"DialogList.SearchSectionDialogs" = "Chats and Contacts";
|
||||
"DialogList.SearchSectionChats" = "Chats";
|
||||
"DialogList.SearchSectionGlobal" = "Global Search";
|
||||
"DialogList.SearchSectionMessages" = "Messages";
|
||||
|
||||
@ -3995,6 +3998,7 @@ Unused sets are archived when you add more.";
|
||||
"ChatList.DeleteChatConfirmation" = "Are you sure you want to delete chat\nwith %@?";
|
||||
"ChatList.DeleteSecretChatConfirmation" = "Are you sure you want to delete secret chat\nwith %@?";
|
||||
"ChatList.LeaveGroupConfirmation" = "Are you sure you want to leave %@?";
|
||||
"ChatList.DeleteAndLeaveGroupConfirmation" = "Are you sure you want to leave and delete %@?";
|
||||
"ChatList.DeleteSavedMessagesConfirmation" = "Are you sure you want to delete\nSaved Messages?";
|
||||
|
||||
"Undo.Undo" = "Undo";
|
||||
@ -4272,6 +4276,10 @@ Unused sets are archived when you add more.";
|
||||
"ChatList.DeleteForEveryoneConfirmationTitle" = "Warning!";
|
||||
"ChatList.DeleteForEveryoneConfirmationText" = "This will **delete all messages** in this chat for **both participants**.";
|
||||
"ChatList.DeleteForEveryoneConfirmationAction" = "Delete All";
|
||||
"ChatList.DeleteForAllMembers" = "Delete for all members";
|
||||
"ChatList.DeleteForAllSubscribers" = "Delete for all subscribers";
|
||||
"ChatList.DeleteForAllMembersConfirmationText" = "This will **delete all messages** in this chat for **all participants**.";
|
||||
"ChatList.DeleteForAllSubscribersConfirmationText" = "This will **delete all messages** in this channel for **all subscribers**.";
|
||||
|
||||
"ChatList.DeleteSavedMessagesConfirmationTitle" = "Warning!";
|
||||
"ChatList.DeleteSavedMessagesConfirmationText" = "This will **delete all messages** in this chat.";
|
||||
@ -5816,3 +5824,157 @@ Sorry for the inconvenience.";
|
||||
|
||||
"PeerInfo.ButtonVoiceChat" = "Voice Chat";
|
||||
"VoiceChat.OpenChat" = "Open Chat";
|
||||
|
||||
"GroupInfo.InviteLinks" = "Invite Links";
|
||||
|
||||
"InviteLink.Title" = "Invite Links";
|
||||
"InviteLink.PermanentLink" = "Permanent Link";
|
||||
"InviteLink.PublicLink" = "Public Link";
|
||||
"InviteLink.Share" = "Share Link";
|
||||
"InviteLink.PeopleJoinedNone" = "no one joined yet";
|
||||
"InviteLink.PeopleJoined_1" = "%@ people joined";
|
||||
"InviteLink.PeopleJoined_2" = "%@ people joined";
|
||||
"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.Manage" = "Manage Invite Links";
|
||||
|
||||
"InviteLink.PeopleJoinedShortNoneExpired" = "no one joined";
|
||||
"InviteLink.PeopleJoinedShortNone" = "no one joined yet";
|
||||
"InviteLink.PeopleJoinedShort_1" = "%@ joined";
|
||||
"InviteLink.PeopleJoinedShort_2" = "%@ joined";
|
||||
"InviteLink.PeopleJoinedShort_3_10" = "%@ joined";
|
||||
"InviteLink.PeopleJoinedShort_many" = "%@ joined";
|
||||
"InviteLink.PeopleJoinedShort_any" = "%@ joined";
|
||||
|
||||
"InviteLink.Expired" = "expired";
|
||||
"InviteLink.UsageLimitReached" = "limit reached";
|
||||
"InviteLink.Revoked" = "revoked";
|
||||
"InviteLink.TapToCopy" = "tap to copy";
|
||||
|
||||
"InviteLink.AdditionalLinks" = "Additional Links";
|
||||
"InviteLink.Create" = "Create a New Link";
|
||||
"InviteLink.CreateInfo" = "You can create additional invite links that have limited time or number of usages.";
|
||||
|
||||
"InviteLink.RevokedLinks" = "Revoked Links";
|
||||
"InviteLink.DeleteAllRevokedLinks" = "Delete All Revoked Links";
|
||||
|
||||
"InviteLink.ContextCopy" = "Copy";
|
||||
"InviteLink.ContextEdit" = "Edit";
|
||||
"InviteLink.ContextGetQRCode" = "Get QR Code";
|
||||
"InviteLink.ContextShare" = "Share";
|
||||
"InviteLink.ContextRevoke" = "Revoke";
|
||||
"InviteLink.ContextDelete" = "Delete";
|
||||
|
||||
"InviteLink.Create.Title" = "New Link";
|
||||
"InviteLink.Create.EditTitle" = "Edit Link";
|
||||
|
||||
"InviteLink.Create.TimeLimit" = "Limit By Time Period";
|
||||
"InviteLink.Create.TimeLimitExpiryDate" = "Expiry Date";
|
||||
"InviteLink.Create.TimeLimitExpiryDateNever" = "Never";
|
||||
"InviteLink.Create.TimeLimitExpiryTime" = "Time";
|
||||
"InviteLink.Create.TimeLimitInfo" = "You can make the link expire after a certain time.";
|
||||
"InviteLink.Create.TimeLimitNoLimit" = "No Limit";
|
||||
|
||||
"InviteLink.Create.UsersLimit" = "Limit By Number Of Users";
|
||||
"InviteLink.Create.UsersLimitNumberOfUsers" = "Number of Uses";
|
||||
"InviteLink.Create.UsersLimitNumberOfUsersUnlimited" = "Unlimited";
|
||||
"InviteLink.Create.UsersLimitInfo" = "You can make the link expire after it has been used for a certain number of times.";
|
||||
"InviteLink.Create.UsersLimitNoLimit" = "No Limit";
|
||||
|
||||
"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.Share" = "Share QR Code";
|
||||
|
||||
"InviteLink.InviteLink" = "Invite Link";
|
||||
"InviteLink.CreatedBy" = "Link Created By";
|
||||
|
||||
"InviteLink.DeleteLinkAlert.Text" = "Are you sure you want to delete this link? It will be completely gone.";
|
||||
"InviteLink.DeleteLinkAlert.Action" = "Delete";
|
||||
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links.";
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete All";
|
||||
|
||||
"InviteLink.ExpiresIn" = "expires in %@";
|
||||
|
||||
"InviteLink.InviteLinkCopiedText" = "Invite link copied to clipboard";
|
||||
|
||||
"Conversation.ChecksTooltip.Delivered" = "Delivered";
|
||||
"Conversation.ChecksTooltip.Read" = "Read";
|
||||
|
||||
"DialogList.MultipleTypingPair" = "%@ and %@ are typing";
|
||||
|
||||
"Common.Save" = "Save";
|
||||
|
||||
"UserInfo.FakeUserWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
|
||||
"UserInfo.FakeBotWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
|
||||
"GroupInfo.FakeGroupWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
|
||||
"ChannelInfo.FakeChannelWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
|
||||
|
||||
"ReportPeer.ReasonFake" = "Fake Account";
|
||||
|
||||
"ChatList.HeaderImportIntoAnExistingGroup" = "SELECT A CHAT TO IMPORT MESSAGES TO";
|
||||
|
||||
"Group.ErrorAdminsTooMuch" = "Sorry, too many administrators in this group.";
|
||||
"Channel.ErrorAdminsTooMuch" = "Sorry, too many administrators in this channel.";
|
||||
|
||||
"Conversation.AddMembers" = "Add Members";
|
||||
|
||||
"Conversation.ImportedMessageHint" = "This message was imported from another app. We can't guarantee it's real.";
|
||||
|
||||
"Conversation.GreetingText" = "Send a message or tap on the greeting below.";
|
||||
|
||||
"CallList.DeleteAllForMe" = "Delete for me";
|
||||
"CallList.DeleteAllForEveryone" = "Delete for me and Others";
|
||||
"Conversation.ImportProgress" = "Importing Messages... %@%";
|
||||
|
||||
"Conversation.AudioRateTooltipSpeedUp" = "Audio will play two times faster.";
|
||||
"Conversation.AudioRateTooltipNormal" = "Audio will play at normal speed.";
|
||||
|
||||
"ChatImport.Title" = "Select Chat";
|
||||
"ChatImport.SelectionErrorNotAdmin" = "You must to be an admin in the group to import messages to it.";
|
||||
"ChatImport.SelectionErrorGroupGeneric" = "Sorry, you can't import history to this group.";
|
||||
"ChatImport.SelectionConfirmationGroupWithTitle" = "Do you want to import messages from **%1$@** into **%2$@**?";
|
||||
"ChatImport.SelectionConfirmationGroupWithoutTitle" = "Do you want to import messages into **%@**?";
|
||||
"ChatImport.SelectionConfirmationAlertTitle" = "Import Messages";
|
||||
"ChatImport.SelectionConfirmationAlertImportAction" = "Import";
|
||||
"ChatImport.CreateGroupAlertTitle" = "Create Group and Import Messages";
|
||||
"ChatImport.CreateGroupAlertText" = "Do you want to create the group **%@** and import messages from another messaging app?";
|
||||
"ChatImport.CreateGroupAlertImportAction" = "Create and Import";
|
||||
"ChatImport.UserErrorNotMutual" = "You can only import messages into private chats with users who are mutual contacts.";
|
||||
"ChatImport.SelectionConfirmationUserWithTitle" = "Do you want to import messages from **%1$@** into the chat with **%2$@**?";
|
||||
"ChatImport.SelectionConfirmationUserWithoutTitle" = "Do you want to import messages into the chat with **%@?**";
|
||||
|
||||
"PeerSelection.ImportIntoNewGroup" = "Import to a New Group";
|
||||
"Message.ImportedDateFormat" = "%1$@, %2$@ Imported %3$@";
|
||||
|
||||
"ChatImportActivity.Title" = "Importing Chat";
|
||||
"ChatImportActivity.OpenApp" = "Open Telegram";
|
||||
"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.";
|
||||
"ChatImportActivity.ErrorInvalidChatType" = "Wrong type of chat for the messages you are trying to import.";
|
||||
"ChatImportActivity.ErrorUserBlocked" = "Unable to import messages due to privacy settings.";
|
||||
"ChatImportActivity.ErrorGeneric" = "An error occurred.";
|
||||
"ChatImportActivity.Success" = "Chat imported\nsuccessfully.";
|
||||
|
||||
"VoiceOver.Chat.GoToOriginalMessage" = "Go to message";
|
||||
"VoiceOver.Chat.UnreadMessages_0" = "%@ unread messages";
|
||||
"VoiceOver.Chat.UnreadMessages_1" = "%@ unread message";
|
||||
"VoiceOver.Chat.UnreadMessages_2" = "%@ unread messages";
|
||||
"VoiceOver.Chat.UnreadMessages_3_10" = "%@ unread messages";
|
||||
"VoiceOver.Chat.UnreadMessages_many" = "%@ unread messages";
|
||||
"VoiceOver.Chat.UnreadMessages_any" = "%@ unread messages";
|
||||
|
||||
"VoiceOver.ChatList.Message" = "Message";
|
||||
"VoiceOver.ChatList.OutgoingMessage" = "Outgoing Message";
|
||||
"VoiceOver.ChatList.MessageFrom" = "From: %@";
|
||||
"VoiceOver.ChatList.MessageRead" = "Read";
|
||||
"VoiceOver.ChatList.MessageEmpty" = "Empty";
|
||||
|
||||
"VoiceOver.Chat.Profile" = "Profile";
|
||||
"VoiceOver.Chat.GroupInfo" = "Group Info";
|
||||
"VoiceOver.Chat.ChannelInfo" = "Channel Info";
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. ";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями.";
|
||||
"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео.";
|
||||
"NSPhotoLibraryUsageDescription" = "Это необходимо, чтобы вы могли делиться фото и видео из библиотеки устройства.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Это необходимо, чтобы вы могли сохранять фото и видео в библиотеку устройства.";
|
||||
"NSMicrophoneUsageDescription" = "Это необходимо, чтобы вы могли делиться голосовыми сообщениями и видео со звуком.";
|
||||
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений";
|
||||
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений.";
|
||||
"NSFaceIDUsageDescription" = "Вы можете разблокировать приложение с помощью Face ID.";
|
||||
|
@ -13,6 +13,9 @@ import Postbox
|
||||
import SyncCore
|
||||
import TelegramCore
|
||||
import OpenSSLEncryptionProvider
|
||||
import WidgetItemsUtils
|
||||
|
||||
import GeneratedSources
|
||||
|
||||
private var installedSharedLogger = false
|
||||
|
||||
@ -156,9 +159,16 @@ struct Provider: IntentTimelineProvider {
|
||||
)
|
||||
}
|
||||
|
||||
var mappedMessage: WidgetDataPeer.Message?
|
||||
if let index = transaction.getTopPeerMessageIndex(peerId: peer.id) {
|
||||
if let message = transaction.getMessage(index.id) {
|
||||
mappedMessage = WidgetDataPeer.Message(message: message)
|
||||
}
|
||||
}
|
||||
|
||||
peers.append(WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
|
||||
return postbox.mediaBox.resourcePath(representation.resource)
|
||||
}, badge: badge))
|
||||
}, badge: badge, message: mappedMessage))
|
||||
}
|
||||
}
|
||||
return WidgetDataPeers(accountPeerId: widgetPeers.accountPeerId, peers: peers)
|
||||
@ -195,12 +205,13 @@ struct AvatarItemView: View {
|
||||
var accountPeerId: Int64
|
||||
var peer: WidgetDataPeer
|
||||
var itemSize: CGFloat
|
||||
var displayBadge: Bool = true
|
||||
|
||||
var body: some View {
|
||||
return ZStack {
|
||||
Image(uiImage: avatarImage(accountPeerId: accountPeerId, peer: peer, size: CGSize(width: itemSize, height: itemSize)))
|
||||
.clipShape(Circle())
|
||||
if let badge = peer.badge, badge.count > 0 {
|
||||
if displayBadge, let badge = peer.badge, badge.count > 0 {
|
||||
Text("\(badge.count)")
|
||||
.font(Font.system(size: 16.0))
|
||||
.multilineTextAlignment(.center)
|
||||
@ -219,6 +230,7 @@ struct AvatarItemView: View {
|
||||
|
||||
struct WidgetView: View {
|
||||
@Environment(\.widgetFamily) private var widgetFamily
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let data: PeersWidgetData
|
||||
|
||||
func placeholder(geometry: GeometryProxy) -> some View {
|
||||
@ -329,12 +341,150 @@ struct WidgetView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
var body1: some View {
|
||||
ZStack {
|
||||
peerViews()
|
||||
}
|
||||
.padding(0.0)
|
||||
}
|
||||
|
||||
func chatTopLine(_ peer: WidgetDataPeer) -> some View {
|
||||
let dateText: String
|
||||
if let message = peer.message {
|
||||
dateText = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .none, timeStyle: .short)
|
||||
} else {
|
||||
dateText = ""
|
||||
}
|
||||
return HStack(alignment: .center, spacing: 0.0, content: {
|
||||
Text(peer.name).font(Font.system(size: 16.0, weight: .medium, design: .default)).foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(dateText).font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary)
|
||||
})
|
||||
}
|
||||
|
||||
func chatBottomLine(_ peer: WidgetDataPeer) -> some View {
|
||||
var text = peer.message?.text ?? ""
|
||||
if let message = peer.message {
|
||||
//TODO:localize
|
||||
switch message.content {
|
||||
case .text:
|
||||
break
|
||||
case .image:
|
||||
text = "🖼 Photo"
|
||||
case .video:
|
||||
text = "📹 Video"
|
||||
case .gif:
|
||||
text = "Gif"
|
||||
case let .file(file):
|
||||
text = "📎 \(file.name)"
|
||||
case let .music(music):
|
||||
if !music.title.isEmpty && !music.artist.isEmpty {
|
||||
text = "\(music.artist) — \(music.title)"
|
||||
} else if !music.title.isEmpty {
|
||||
text = music.title
|
||||
} else if !music.artist.isEmpty {
|
||||
text = music.artist
|
||||
} else {
|
||||
text = "Music"
|
||||
}
|
||||
case .voiceMessage:
|
||||
text = "🎤 Voice Message"
|
||||
case .videoMessage:
|
||||
text = "Video Message"
|
||||
case let .sticker(sticker):
|
||||
text = "\(sticker.altText) Sticker"
|
||||
case let .call(call):
|
||||
if call.isVideo {
|
||||
text = "Video Call"
|
||||
} else {
|
||||
text = "Voice Call"
|
||||
}
|
||||
case .mapLocation:
|
||||
text = "Location"
|
||||
case let .game(game):
|
||||
text = "🎮 \(game.title)"
|
||||
case let .poll(poll):
|
||||
text = "📊 \(poll.title)"
|
||||
}
|
||||
}
|
||||
|
||||
var hasBadge = false
|
||||
if let badge = peer.badge, badge.count > 0 {
|
||||
hasBadge = true
|
||||
}
|
||||
|
||||
return HStack(alignment: .center, spacing: hasBadge ? 6.0 : 0.0, content: {
|
||||
Text(text).lineLimit(nil).font(Font.system(size: 15.0, weight: .regular, design: .default)).foregroundColor(.secondary).multilineTextAlignment(.leading).frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
Spacer()
|
||||
if let badge = peer.badge, badge.count > 0 {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("\(badge.count)")
|
||||
.font(Font.system(size: 14.0))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4.0)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(badge.isMuted ? Color.gray : Color.blue)
|
||||
.frame(minWidth: 20, idealWidth: 20, maxWidth: .infinity, minHeight: 20, idealHeight: 20, maxHeight: 20.0, alignment: .center)
|
||||
)
|
||||
.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 6.0, trailing: 3.0))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func chatContent(_ peer: WidgetDataPeer) -> some View {
|
||||
return VStack(alignment: .leading, spacing: 2.0, content: {
|
||||
chatTopLine(peer)
|
||||
chatBottomLine(peer).frame(maxHeight: .infinity)
|
||||
})
|
||||
}
|
||||
|
||||
func chatContentView(_ index: Int) -> AnyView {
|
||||
let peers: WidgetDataPeers
|
||||
switch data {
|
||||
case let .peers(peersValue):
|
||||
peers = peersValue
|
||||
if peers.peers.count <= index {
|
||||
return AnyView(Spacer())
|
||||
}
|
||||
default:
|
||||
return AnyView(Spacer())
|
||||
}
|
||||
|
||||
return AnyView(
|
||||
Link(destination: URL(string: linkForPeer(id: peers.peers[index].id))!, label: {
|
||||
HStack(alignment: .center, spacing: 0.0, content: {
|
||||
AvatarItemView(accountPeerId: peers.accountPeerId, peer: peers.peers[index], itemSize: 60.0, displayBadge: false).frame(width: 60.0, height: 60.0, alignment: .leading).padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
|
||||
chatContent(peers.peers[index]).frame(maxWidth: .infinity).padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 10.0))
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
func getSeparatorColor() -> Color {
|
||||
switch colorScheme {
|
||||
case .light:
|
||||
return Color(.sRGB, red: 200.0 / 255.0, green: 199.0 / 255.0, blue: 204.0 / 255.0, opacity: 1.0)
|
||||
case .dark:
|
||||
return Color(.sRGB, red: 61.0 / 255.0, green: 61.0 / 255.0, blue: 64.0 / 255.0, opacity: 1.0)
|
||||
@unknown default:
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader(content: { geometry in
|
||||
ZStack {
|
||||
chatContentView(0).position(x: geometry.size.width / 2.0, y: geometry.size.height / 4.0).frame(width: geometry.size.width, height: geometry.size.height / 2.0, alignment: .leading)
|
||||
chatContentView(1).position(x: geometry.size.width / 2.0, y: geometry.size.height / 2.0 + geometry.size.height / 4.0).frame(width: geometry.size.width, height: geometry.size.height / 2.0, alignment: .leading)
|
||||
Rectangle().foregroundColor(getSeparatorColor()).position(x: geometry.size.width / 2.0, y: geometry.size.height / 4.0).frame(width: geometry.size.width, height: 0.33, alignment: .leading)
|
||||
}
|
||||
})
|
||||
.padding(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
private let buildConfig: BuildConfig = {
|
||||
@ -429,7 +579,7 @@ struct Static_Widget: Widget {
|
||||
return IntentConfiguration(kind: kind, intent: SelectFriendsIntent.self, provider: Provider(), content: { entry in
|
||||
WidgetView(data: getWidgetData(contents: entry.contents))
|
||||
})
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
.supportedFamilies([.systemMedium])
|
||||
.configurationDisplayName(presentationData.widgetGalleryTitle)
|
||||
.description(presentationData.widgetGalleryDescription)
|
||||
}
|
||||
|
148
build-system/Make/BuildEnvironment.py
Normal file
148
build-system/Make/BuildEnvironment.py
Normal file
@ -0,0 +1,148 @@
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
|
||||
def is_apple_silicon():
|
||||
if platform.processor() == 'arm':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_clean_env():
|
||||
clean_env = os.environ.copy()
|
||||
clean_env['PATH'] = '/usr/bin:/bin:/usr/sbin:/sbin'
|
||||
return clean_env
|
||||
|
||||
|
||||
def resolve_executable(program):
|
||||
def is_executable(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
for path in get_clean_env()["PATH"].split(os.pathsep):
|
||||
executable_file = os.path.join(path, program)
|
||||
if is_executable(executable_file):
|
||||
return executable_file
|
||||
return None
|
||||
|
||||
|
||||
def run_executable_with_output(path, arguments):
|
||||
executable_path = resolve_executable(path)
|
||||
if executable_path is None:
|
||||
raise Exception('Could not resolve {} to a valid executable file'.format(path))
|
||||
|
||||
process = subprocess.Popen(
|
||||
[executable_path] + arguments,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=get_clean_env()
|
||||
)
|
||||
output_data, _ = process.communicate()
|
||||
output_string = output_data.decode('utf-8')
|
||||
return output_string
|
||||
|
||||
|
||||
def call_executable(arguments, use_clean_environment=True, check_result=True):
|
||||
executable_path = resolve_executable(arguments[0])
|
||||
if executable_path is None:
|
||||
raise Exception('Could not resolve {} to a valid executable file'.format(arguments[0]))
|
||||
|
||||
if use_clean_environment:
|
||||
resolved_env = get_clean_env()
|
||||
else:
|
||||
resolved_env = os.environ
|
||||
|
||||
resolved_arguments = [executable_path] + arguments[1:]
|
||||
|
||||
if check_result:
|
||||
subprocess.check_call(resolved_arguments, env=resolved_env)
|
||||
else:
|
||||
subprocess.call(resolved_arguments, env=resolved_env)
|
||||
|
||||
|
||||
def get_bazel_version(bazel_path):
|
||||
command_result = run_executable_with_output(bazel_path, ['--version']).strip('\n')
|
||||
if not command_result.startswith('bazel '):
|
||||
raise Exception('{} is not a valid bazel binary'.format(bazel_path))
|
||||
command_result = command_result.replace('bazel ', '')
|
||||
return command_result
|
||||
|
||||
|
||||
def get_xcode_version():
|
||||
xcode_path = run_executable_with_output('xcode-select', ['-p']).strip('\n')
|
||||
if not os.path.isdir(xcode_path):
|
||||
print('The path reported by \'xcode-select -p\' does not exist')
|
||||
exit(1)
|
||||
|
||||
plist_path = '{}/../Info.plist'.format(xcode_path)
|
||||
|
||||
info_plist_lines = run_executable_with_output('plutil', [
|
||||
'-p', plist_path
|
||||
]).split('\n')
|
||||
|
||||
pattern = 'CFBundleShortVersionString" => '
|
||||
for line in info_plist_lines:
|
||||
index = line.find(pattern)
|
||||
if index != -1:
|
||||
version = line[index + len(pattern):].strip('"')
|
||||
return version
|
||||
|
||||
print('Could not parse the Xcode version from {}'.format(plist_path))
|
||||
exit(1)
|
||||
|
||||
|
||||
class BuildEnvironment:
|
||||
def __init__(
|
||||
self,
|
||||
base_path,
|
||||
bazel_path,
|
||||
bazel_x86_64_path,
|
||||
override_bazel_version,
|
||||
override_xcode_version
|
||||
):
|
||||
self.base_path = os.path.expanduser(base_path)
|
||||
self.bazel_path = os.path.expanduser(bazel_path)
|
||||
if bazel_x86_64_path is not None:
|
||||
self.bazel_x86_64_path = os.path.expanduser(bazel_x86_64_path)
|
||||
else:
|
||||
self.bazel_x86_64_path = None
|
||||
|
||||
configuration_path = os.path.join(self.base_path, 'versions.json')
|
||||
with open(configuration_path) as file:
|
||||
configuration_dict = json.load(file)
|
||||
if configuration_dict['app'] is None:
|
||||
raise Exception('Missing app version in {}'.format(configuration_path))
|
||||
else:
|
||||
self.app_version = configuration_dict['app']
|
||||
if configuration_dict['bazel'] is None:
|
||||
raise Exception('Missing bazel version in {}'.format(configuration_path))
|
||||
else:
|
||||
self.bazel_version = configuration_dict['bazel']
|
||||
if configuration_dict['xcode'] is None:
|
||||
raise Exception('Missing xcode version in {}'.format(configuration_path))
|
||||
else:
|
||||
self.xcode_version = configuration_dict['xcode']
|
||||
|
||||
actual_bazel_version = get_bazel_version(self.bazel_path)
|
||||
if actual_bazel_version != self.bazel_version:
|
||||
if override_bazel_version:
|
||||
print('Overriding the required bazel version {} with {} as reported by {}'.format(
|
||||
self.bazel_version, actual_bazel_version, self.bazel_path))
|
||||
self.bazel_version = actual_bazel_version
|
||||
else:
|
||||
print('Required bazel version is "{}", but "{}"" is reported by {}'.format(
|
||||
self.bazel_version, actual_bazel_version, self.bazel_path))
|
||||
exit(1)
|
||||
|
||||
actual_xcode_version = get_xcode_version()
|
||||
if actual_xcode_version != self.xcode_version:
|
||||
if override_xcode_version:
|
||||
print('Overriding the required Xcode version {} with {} as reported by \'xcode-select -p\''.format(
|
||||
self.xcode_version, actual_xcode_version, self.bazel_path))
|
||||
self.xcode_version = actual_xcode_version
|
||||
else:
|
||||
print('Required Xcode version is {}, but {} is reported by \'xcode-select -p\''.format(
|
||||
self.xcode_version, actual_xcode_version, self.bazel_path))
|
||||
exit(1)
|
508
build-system/Make/Make.py
Normal file
508
build-system/Make/Make.py
Normal file
@ -0,0 +1,508 @@
|
||||
#!/bin/python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
from BuildEnvironment import is_apple_silicon, resolve_executable, call_executable, BuildEnvironment
|
||||
from ProjectGeneration import generate
|
||||
|
||||
|
||||
class BazelCommandLine:
|
||||
def __init__(self, bazel_path, bazel_x86_64_path, override_bazel_version, override_xcode_version):
|
||||
self.build_environment = BuildEnvironment(
|
||||
base_path=os.getcwd(),
|
||||
bazel_path=bazel_path,
|
||||
bazel_x86_64_path=bazel_x86_64_path,
|
||||
override_bazel_version=override_bazel_version,
|
||||
override_xcode_version=override_xcode_version
|
||||
)
|
||||
self.remote_cache = None
|
||||
self.cache_dir = None
|
||||
self.additional_args = None
|
||||
self.build_number = None
|
||||
self.configuration_args = None
|
||||
self.configuration_path = None
|
||||
|
||||
self.common_args = [
|
||||
# https://docs.bazel.build/versions/master/command-line-reference.html
|
||||
# Ask bazel to print the actual resolved command line options.
|
||||
'--announce_rc',
|
||||
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# If enabled, Swift compilation actions will use the same global Clang module
|
||||
# cache used by Objective-C compilation actions. This is disabled by default
|
||||
# because under some circumstances Clang module cache corruption can cause the
|
||||
# Swift compiler to crash (sometimes when switching configurations or syncing a
|
||||
# repository), but disabling it also causes a noticeable build time regression
|
||||
# so it can be explicitly re-enabled by users who are not affected by those
|
||||
# crashes.
|
||||
'--features=swift.use_global_module_cache',
|
||||
|
||||
# https://docs.bazel.build/versions/master/command-line-reference.html
|
||||
# Print the subcommand details in case of failure.
|
||||
'--verbose_failures',
|
||||
]
|
||||
|
||||
self.common_build_args = [
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# If enabled and whole module optimisation is being used, the `*.swiftdoc`,
|
||||
# `*.swiftmodule` and `*-Swift.h` are generated with a separate action
|
||||
# rather than as part of the compilation.
|
||||
'--features=swift.split_derived_files_generation',
|
||||
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# If enabled the skip function bodies frontend flag is passed when using derived
|
||||
# files generation.
|
||||
'--features=swift.skip_function_bodies_for_derived_files',
|
||||
|
||||
# Set the number of parallel processes to match the available CPU core count.
|
||||
'--jobs={}'.format(os.cpu_count()),
|
||||
]
|
||||
|
||||
self.common_debug_args = [
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# If enabled, Swift compilation actions will use batch mode by passing
|
||||
# `-enable-batch-mode` to `swiftc`. This is a new compilation mode as of
|
||||
# Swift 4.2 that is intended to speed up non-incremental non-WMO builds by
|
||||
# invoking a smaller number of frontend processes and passing them batches of
|
||||
# source files.
|
||||
'--features=swift.enable_batch_mode',
|
||||
|
||||
# https://docs.bazel.build/versions/master/command-line-reference.html
|
||||
# Set the number of parallel jobs per module to saturate the available CPU resources.
|
||||
'--swiftcopt=-j{}'.format(os.cpu_count() - 1),
|
||||
]
|
||||
|
||||
self.common_release_args = [
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# Enable whole module optimization.
|
||||
'--features=swift.opt_uses_wmo',
|
||||
|
||||
# https://github.com/bazelbuild/rules_swift
|
||||
# Use -Osize instead of -O when building swift modules.
|
||||
'--features=swift.opt_uses_osize',
|
||||
|
||||
# --num-threads 0 forces swiftc to generate one object file per module; it:
|
||||
# 1. resolves issues with the linker caused by the swift-objc mixing.
|
||||
# 2. makes the resulting binaries significantly smaller (up to 9% for this project).
|
||||
'--swiftcopt=-num-threads', '--swiftcopt=0',
|
||||
|
||||
# Strip unsused code.
|
||||
'--features=dead_strip',
|
||||
'--objc_enable_binary_stripping',
|
||||
|
||||
# Always embed bitcode into Watch binaries. This is required by the App Store.
|
||||
'--apple_bitcode=watchos=embedded',
|
||||
]
|
||||
|
||||
def add_remote_cache(self, host):
|
||||
self.remote_cache = host
|
||||
|
||||
def add_cache_dir(self, path):
|
||||
self.cache_dir = path
|
||||
|
||||
def add_additional_args(self, additional_args):
|
||||
self.additional_args = additional_args
|
||||
|
||||
def set_build_number(self, build_number):
|
||||
self.build_number = build_number
|
||||
|
||||
def set_configuration_path(self, path):
|
||||
self.configuration_path = path
|
||||
|
||||
def set_configuration(self, configuration):
|
||||
if configuration == 'debug_arm64':
|
||||
self.configuration_args = [
|
||||
# bazel debug build configuration
|
||||
'-c', 'dbg',
|
||||
|
||||
# Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required.
|
||||
'--ios_multi_cpus=arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=armv7k,arm64_32'
|
||||
] + self.common_debug_args
|
||||
elif configuration == 'debug_armv7':
|
||||
self.configuration_args = [
|
||||
# bazel debug build configuration
|
||||
'-c', 'dbg',
|
||||
|
||||
'--ios_multi_cpus=armv7',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=armv7k,arm64_32'
|
||||
] + self.common_debug_args
|
||||
elif configuration == 'release_arm64':
|
||||
self.configuration_args = [
|
||||
# bazel optimized build configuration
|
||||
'-c', 'opt',
|
||||
|
||||
# Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required.
|
||||
'--ios_multi_cpus=arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=armv7k,arm64_32',
|
||||
|
||||
# Generate DSYM files when building.
|
||||
'--apple_generate_dsym',
|
||||
|
||||
# Require DSYM files as build output.
|
||||
'--output_groups=+dsyms'
|
||||
] + self.common_release_args
|
||||
elif configuration == 'release_universal':
|
||||
self.configuration_args = [
|
||||
# bazel optimized build configuration
|
||||
'-c', 'opt',
|
||||
|
||||
# Build universal binaries.
|
||||
'--ios_multi_cpus=armv7,arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=armv7k,arm64_32',
|
||||
|
||||
# Generate DSYM files when building.
|
||||
'--apple_generate_dsym',
|
||||
|
||||
# Require DSYM files as build output.
|
||||
'--output_groups=+dsyms'
|
||||
] + self.common_release_args
|
||||
else:
|
||||
raise Exception('Unknown configuration {}'.format(configuration))
|
||||
|
||||
def invoke_clean(self):
|
||||
combined_arguments = [
|
||||
self.build_environment.bazel_path,
|
||||
'clean',
|
||||
'--expunge'
|
||||
]
|
||||
|
||||
print('TelegramBuild: running {}'.format(combined_arguments))
|
||||
call_executable(combined_arguments)
|
||||
|
||||
def get_define_arguments(self):
|
||||
return [
|
||||
'--define=buildNumber={}'.format(self.build_number),
|
||||
'--define=telegramVersion={}'.format(self.build_environment.app_version)
|
||||
]
|
||||
|
||||
def get_project_generation_arguments(self):
|
||||
combined_arguments = []
|
||||
combined_arguments += self.common_args
|
||||
combined_arguments += self.common_debug_args
|
||||
combined_arguments += self.get_define_arguments()
|
||||
|
||||
if self.remote_cache is not None:
|
||||
combined_arguments += [
|
||||
'--remote_cache={}'.format(self.remote_cache),
|
||||
'--experimental_remote_downloader={}'.format(self.remote_cache)
|
||||
]
|
||||
elif self.cache_dir is not None:
|
||||
combined_arguments += [
|
||||
'--disk_cache={path}'.format(path=self.cache_dir)
|
||||
]
|
||||
|
||||
return combined_arguments
|
||||
|
||||
def invoke_build(self):
|
||||
combined_arguments = [
|
||||
self.build_environment.bazel_path,
|
||||
'build',
|
||||
'Telegram/Telegram'
|
||||
]
|
||||
|
||||
if self.configuration_path is None:
|
||||
raise Exception('configuration_path is not defined')
|
||||
|
||||
combined_arguments += [
|
||||
'--override_repository=build_configuration={}'.format(self.configuration_path)
|
||||
]
|
||||
|
||||
combined_arguments += self.common_args
|
||||
combined_arguments += self.common_build_args
|
||||
combined_arguments += self.get_define_arguments()
|
||||
|
||||
if self.remote_cache is not None:
|
||||
combined_arguments += [
|
||||
'--remote_cache={}'.format(self.remote_cache),
|
||||
'--experimental_remote_downloader={}'.format(self.remote_cache)
|
||||
]
|
||||
elif self.cache_dir is not None:
|
||||
combined_arguments += [
|
||||
'--disk_cache={path}'.format(path=self.cache_dir)
|
||||
]
|
||||
|
||||
combined_arguments += self.configuration_args
|
||||
|
||||
print('TelegramBuild: running')
|
||||
print(subprocess.list2cmdline(combined_arguments))
|
||||
call_executable(combined_arguments)
|
||||
|
||||
|
||||
def clean(arguments):
|
||||
bazel_command_line = BazelCommandLine(
|
||||
bazel_path=arguments.bazel,
|
||||
bazel_x86_64_path=None,
|
||||
override_bazel_version=arguments.overrideBazelVersion,
|
||||
override_xcode_version=arguments.overrideXcodeVersion
|
||||
)
|
||||
|
||||
bazel_command_line.invoke_clean()
|
||||
|
||||
|
||||
def resolve_configuration(bazel_command_line: BazelCommandLine, arguments):
|
||||
if arguments.configurationGenerator is not None:
|
||||
configuration_generator_arguments = shlex.split(arguments.configurationGenerator)
|
||||
|
||||
configuration_generator_executable = resolve_executable(configuration_generator_arguments[0])
|
||||
|
||||
if configuration_generator_executable is None:
|
||||
print('{} is not a valid executable'.format(configuration_generator_arguments[0]))
|
||||
exit(1)
|
||||
|
||||
temp_configuration_path = tempfile.mkdtemp()
|
||||
|
||||
resolved_configuration_generator_arguments = [configuration_generator_executable]
|
||||
resolved_configuration_generator_arguments += configuration_generator_arguments[1:]
|
||||
resolved_configuration_generator_arguments += [temp_configuration_path]
|
||||
|
||||
call_executable(resolved_configuration_generator_arguments, use_clean_environment=False)
|
||||
|
||||
print('TelegramBuild: using generated configuration in {}'.format(temp_configuration_path))
|
||||
bazel_command_line.set_configuration_path(temp_configuration_path)
|
||||
elif arguments.configurationPath is not None:
|
||||
absolute_configuration_path = os.path.abspath(arguments.configurationPath)
|
||||
if not os.path.isdir(absolute_configuration_path):
|
||||
print('Error: {} does not exist'.format(absolute_configuration_path))
|
||||
exit(1)
|
||||
bazel_command_line.set_configuration_path(absolute_configuration_path)
|
||||
else:
|
||||
raise Exception('Neither configurationPath nor configurationGenerator are set')
|
||||
|
||||
|
||||
def generate_project(arguments):
|
||||
bazel_x86_64_path = None
|
||||
if is_apple_silicon():
|
||||
bazel_x86_64_path = arguments.bazel_x86_64
|
||||
|
||||
bazel_command_line = BazelCommandLine(
|
||||
bazel_path=arguments.bazel,
|
||||
bazel_x86_64_path=bazel_x86_64_path,
|
||||
override_bazel_version=arguments.overrideBazelVersion,
|
||||
override_xcode_version=arguments.overrideXcodeVersion
|
||||
)
|
||||
|
||||
if arguments.cacheDir is not None:
|
||||
bazel_command_line.add_cache_dir(arguments.cacheDir)
|
||||
elif arguments.cacheHost is not None:
|
||||
bazel_command_line.add_remote_cache(arguments.cacheHost)
|
||||
|
||||
resolve_configuration(bazel_command_line, arguments)
|
||||
|
||||
bazel_command_line.set_build_number(arguments.buildNumber)
|
||||
|
||||
disable_extensions = False
|
||||
if arguments.disableExtensions is not None:
|
||||
disable_extensions = arguments.disableExtensions
|
||||
if arguments.disableProvisioningProfiles is not None:
|
||||
disable_provisioning_profiles = arguments.disableProvisioningProfiles
|
||||
|
||||
call_executable(['killall', 'Xcode'], check_result=False)
|
||||
|
||||
generate(
|
||||
build_environment=bazel_command_line.build_environment,
|
||||
disable_extensions=disable_extensions,
|
||||
disable_provisioning_profiles=disable_provisioning_profiles,
|
||||
configuration_path=bazel_command_line.configuration_path,
|
||||
bazel_app_arguments=bazel_command_line.get_project_generation_arguments()
|
||||
)
|
||||
|
||||
|
||||
def build(arguments):
|
||||
bazel_command_line = BazelCommandLine(
|
||||
bazel_path=arguments.bazel,
|
||||
bazel_x86_64_path=None,
|
||||
override_bazel_version=arguments.overrideBazelVersion,
|
||||
override_xcode_version=arguments.overrideXcodeVersion
|
||||
)
|
||||
|
||||
if arguments.cacheDir is not None:
|
||||
bazel_command_line.add_cache_dir(arguments.cacheDir)
|
||||
elif arguments.cacheHost is not None:
|
||||
bazel_command_line.add_remote_cache(arguments.cacheHost)
|
||||
|
||||
resolve_configuration(bazel_command_line, arguments)
|
||||
|
||||
bazel_command_line.set_configuration(arguments.configuration)
|
||||
bazel_command_line.set_build_number(arguments.buildNumber)
|
||||
|
||||
bazel_command_line.invoke_build()
|
||||
|
||||
|
||||
def add_project_and_build_common_arguments(current_parser: argparse.ArgumentParser):
|
||||
group = current_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'--configurationPath',
|
||||
help='''
|
||||
Path to a folder containing build configuration and provisioning profiles.
|
||||
See build-system/example-configuration for an example.
|
||||
''',
|
||||
metavar='path'
|
||||
)
|
||||
group.add_argument(
|
||||
'--configurationGenerator',
|
||||
help='''
|
||||
A command line invocation that will dynamically generate the configuration data
|
||||
(project constants and provisioning profiles).
|
||||
The expression will be parsed according to the shell parsing rules into program and arguments parts.
|
||||
The program will be then invoked with the given arguments plus the path to the output directory.
|
||||
See build-system/generate-configuration.sh for an example.
|
||||
Example: --configurationGenerator="sh ~/my_script.sh argument1"
|
||||
''',
|
||||
metavar='command'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog='Make')
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Print debug info'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--bazel',
|
||||
required=True,
|
||||
help='Use custom bazel binary',
|
||||
metavar='path'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--overrideBazelVersion',
|
||||
action='store_true',
|
||||
help='Override bazel version with the actual version reported by the bazel binary'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--overrideXcodeVersion',
|
||||
action='store_true',
|
||||
help='Override xcode version with the actual version reported by \'xcode-select -p\''
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--bazelArguments',
|
||||
required=False,
|
||||
help='Add additional arguments to all bazel invocations.',
|
||||
metavar='arguments'
|
||||
)
|
||||
|
||||
cacheTypeGroup = parser.add_mutually_exclusive_group()
|
||||
cacheTypeGroup.add_argument(
|
||||
'--cacheHost',
|
||||
required=False,
|
||||
help='Use remote build artifact cache to speed up rebuilds (See https://github.com/buchgr/bazel-remote).',
|
||||
metavar='http://host:9092'
|
||||
)
|
||||
cacheTypeGroup.add_argument(
|
||||
'--cacheDir',
|
||||
required=False,
|
||||
help='Cache build artifacts in a local directory to speed up rebuilds.',
|
||||
metavar='path'
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='commandName', help='Commands')
|
||||
|
||||
cleanParser = subparsers.add_parser(
|
||||
'clean', help='''
|
||||
Clean local bazel cache. Does not affect files cached remotely (via --cacheHost=...) or
|
||||
locally in an external directory ('--cacheDir=...')
|
||||
'''
|
||||
)
|
||||
|
||||
generateProjectParser = subparsers.add_parser('generateProject', help='Generate Xcode project')
|
||||
if is_apple_silicon():
|
||||
generateProjectParser.add_argument(
|
||||
'--bazel_x86_64',
|
||||
required=True,
|
||||
help='A standalone bazel x86_64 binary is required to generate a project on Apple Silicon.',
|
||||
metavar='path'
|
||||
)
|
||||
generateProjectParser.add_argument(
|
||||
'--buildNumber',
|
||||
required=False,
|
||||
type=int,
|
||||
default=10000,
|
||||
help='Build number.',
|
||||
metavar='number'
|
||||
)
|
||||
add_project_and_build_common_arguments(generateProjectParser)
|
||||
generateProjectParser.add_argument(
|
||||
'--disableExtensions',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
The generated project will not include app extensions.
|
||||
This allows Xcode to properly index the source code.
|
||||
'''
|
||||
)
|
||||
|
||||
generateProjectParser.add_argument(
|
||||
'--disableProvisioningProfiles',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
This allows to build the project for simulator without having any codesigning identities installed.
|
||||
Building for an actual device will fail.
|
||||
'''
|
||||
)
|
||||
|
||||
buildParser = subparsers.add_parser('build', help='Build the app')
|
||||
buildParser.add_argument(
|
||||
'--buildNumber',
|
||||
required=True,
|
||||
type=int,
|
||||
help='Build number.',
|
||||
metavar='number'
|
||||
)
|
||||
add_project_and_build_common_arguments(buildParser)
|
||||
buildParser.add_argument(
|
||||
'--configuration',
|
||||
choices=[
|
||||
'debug_arm64',
|
||||
'debug_armv7',
|
||||
'release_arm64',
|
||||
'release_universal'
|
||||
],
|
||||
required=True,
|
||||
help='Build configuration'
|
||||
)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print(args)
|
||||
|
||||
if args.commandName is None:
|
||||
exit(0)
|
||||
|
||||
try:
|
||||
if args.commandName == 'clean':
|
||||
clean(arguments=args)
|
||||
elif args.commandName == 'generateProject':
|
||||
generate_project(arguments=args)
|
||||
elif args.commandName == 'build':
|
||||
build(arguments=args)
|
||||
else:
|
||||
raise Exception('Unknown command')
|
||||
except KeyboardInterrupt:
|
||||
pass
|
147
build-system/Make/ProjectGeneration.py
Normal file
147
build-system/Make/ProjectGeneration.py
Normal file
@ -0,0 +1,147 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from BuildEnvironment import is_apple_silicon, call_executable, BuildEnvironment
|
||||
|
||||
|
||||
def remove_directory(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, configuration_path, bazel_app_arguments):
|
||||
project_path = os.path.join(build_environment.base_path, 'build-input/gen/project')
|
||||
app_target = 'Telegram'
|
||||
|
||||
os.makedirs(project_path, exist_ok=True)
|
||||
remove_directory('{}/Tulsi.app'.format(project_path))
|
||||
remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target))
|
||||
|
||||
tulsi_path = os.path.join(project_path, 'Tulsi.app/Contents/MacOS/Tulsi')
|
||||
|
||||
if is_apple_silicon():
|
||||
tulsi_build_bazel_path = build_environment.bazel_x86_64_path
|
||||
if tulsi_build_bazel_path is None or not os.path.isfile(tulsi_build_bazel_path):
|
||||
print('Could not find a valid bazel x86_64 binary at {}'.format(tulsi_build_bazel_path))
|
||||
exit(1)
|
||||
else:
|
||||
tulsi_build_bazel_path = build_environment.bazel_path
|
||||
|
||||
current_dir = os.getcwd()
|
||||
os.chdir(os.path.join(build_environment.base_path, 'build-system/tulsi'))
|
||||
call_executable([
|
||||
tulsi_build_bazel_path,
|
||||
'build', '//:tulsi',
|
||||
'--xcode_version={}'.format(build_environment.xcode_version),
|
||||
'--use_top_level_targets_for_symlinks',
|
||||
'--verbose_failures'
|
||||
])
|
||||
os.chdir(current_dir)
|
||||
|
||||
bazel_wrapper_path = os.path.abspath('build-input/gen/project/bazel')
|
||||
|
||||
bazel_wrapper_arguments = []
|
||||
bazel_wrapper_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
|
||||
|
||||
with open(bazel_wrapper_path, 'wb') as bazel_wrapper:
|
||||
bazel_wrapper.write('''#!/bin/sh
|
||||
{bazel} "$@" {arguments}
|
||||
'''.format(
|
||||
bazel=build_environment.bazel_path,
|
||||
arguments=' '.join(bazel_wrapper_arguments)
|
||||
).encode('utf-8'))
|
||||
|
||||
call_executable(['chmod', '+x', bazel_wrapper_path])
|
||||
|
||||
call_executable([
|
||||
'unzip', '-oq',
|
||||
'build-system/tulsi/bazel-bin/tulsi.zip',
|
||||
'-d', project_path
|
||||
])
|
||||
|
||||
user_defaults_path = os.path.expanduser('~/Library/Preferences/com.google.Tulsi.plist')
|
||||
if os.path.isfile(user_defaults_path):
|
||||
os.unlink(user_defaults_path)
|
||||
|
||||
with open(user_defaults_path, 'wb') as user_defaults:
|
||||
user_defaults.write('''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>defaultBazelURL</key>
|
||||
<string>{}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
'''.format(bazel_wrapper_path).encode('utf-8'))
|
||||
|
||||
bazel_build_arguments = []
|
||||
bazel_build_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
|
||||
if disable_extensions:
|
||||
bazel_build_arguments += ['--//Telegram:disableExtensions']
|
||||
if disable_provisioning_profiles:
|
||||
bazel_build_arguments += ['--//Telegram:disableProvisioningProfiles']
|
||||
|
||||
call_executable([
|
||||
tulsi_path,
|
||||
'--',
|
||||
'--verbose',
|
||||
'--create-tulsiproj', app_target,
|
||||
'--workspaceroot', './',
|
||||
'--bazel', bazel_wrapper_path,
|
||||
'--outputfolder', project_path,
|
||||
'--target', '{target}:{target}'.format(target=app_target),
|
||||
'--build-options', ' '.join(bazel_build_arguments)
|
||||
])
|
||||
|
||||
additional_arguments = []
|
||||
additional_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
|
||||
additional_arguments += bazel_app_arguments
|
||||
if disable_extensions:
|
||||
additional_arguments += ['--//Telegram:disableExtensions']
|
||||
|
||||
additional_arguments_string = ' '.join(additional_arguments)
|
||||
|
||||
tulsi_config_path = 'build-input/gen/project/{target}.tulsiproj/Configs/{target}.tulsigen'.format(target=app_target)
|
||||
with open(tulsi_config_path, 'rb') as tulsi_config:
|
||||
tulsi_config_json = json.load(tulsi_config)
|
||||
for category in ['BazelBuildOptionsDebug', 'BazelBuildOptionsRelease']:
|
||||
tulsi_config_json['optionSet'][category]['p'] += ' {}'.format(additional_arguments_string)
|
||||
tulsi_config_json['sourceFilters'] = [
|
||||
'Telegram/...',
|
||||
'submodules/...',
|
||||
'third-party/...'
|
||||
]
|
||||
with open(tulsi_config_path, 'wb') as tulsi_config:
|
||||
tulsi_config.write(json.dumps(tulsi_config_json, indent=2).encode('utf-8'))
|
||||
|
||||
call_executable([
|
||||
tulsi_path,
|
||||
'--',
|
||||
'--verbose',
|
||||
'--genconfig', '{project}/{target}.tulsiproj:{target}'.format(project=project_path, target=app_target),
|
||||
'--bazel', bazel_wrapper_path,
|
||||
'--outputfolder', project_path,
|
||||
'--no-open-xcode'
|
||||
])
|
||||
|
||||
xcodeproj_path = '{project}/{target}.xcodeproj'.format(project=project_path, target=app_target)
|
||||
|
||||
bazel_build_settings_path = '{}/.tulsi/Scripts/bazel_build_settings.py'.format(xcodeproj_path)
|
||||
|
||||
with open(bazel_build_settings_path, 'rb') as bazel_build_settings:
|
||||
bazel_build_settings_contents = bazel_build_settings.read().decode('utf-8')
|
||||
bazel_build_settings_contents = bazel_build_settings_contents.replace(
|
||||
'BUILD_SETTINGS = BazelBuildSettings(',
|
||||
'import os\nBUILD_SETTINGS = BazelBuildSettings('
|
||||
)
|
||||
bazel_build_settings_contents = bazel_build_settings_contents.replace(
|
||||
'\'--cpu=ios_arm64\'',
|
||||
'\'--cpu=ios_arm64\'.replace(\'ios_arm64\', \'ios_sim_arm64\' if os.environ.get(\'EFFECTIVE_PLATFORM_NAME\') '
|
||||
'== \'-iphonesimulator\' else \'ios_arm64\')'
|
||||
)
|
||||
with open(bazel_build_settings_path, 'wb') as bazel_build_settings:
|
||||
bazel_build_settings.write(bazel_build_settings_contents.encode('utf-8'))
|
||||
|
||||
call_executable(['open', xcodeproj_path])
|
@ -1 +0,0 @@
|
||||
3.7.0
|
0
build-system/example-configuration/BUILD
Normal file
0
build-system/example-configuration/BUILD
Normal file
0
build-system/example-configuration/WORKSPACE
Normal file
0
build-system/example-configuration/WORKSPACE
Normal file
11
build-system/example-configuration/provisioning/BUILD
Normal file
11
build-system/example-configuration/provisioning/BUILD
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
exports_files([
|
||||
"Intents.mobileprovision",
|
||||
"NotificationContent.mobileprovision",
|
||||
"NotificationService.mobileprovision",
|
||||
"Share.mobileprovision",
|
||||
"Telegram.mobileprovision",
|
||||
"WatchApp.mobileprovision",
|
||||
"WatchExtension.mobileprovision",
|
||||
"Widget.mobileprovision",
|
||||
])
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
11
build-system/example-configuration/variables.bzl
Normal file
11
build-system/example-configuration/variables.bzl
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
telegram_bundle_id = "ph.telegra.Telegraph"
|
||||
telegram_api_id = "8"
|
||||
telegram_api_hash = "7245de8e747a0d6fbe11f7cc14fcc0bb"
|
||||
telegram_team_id = "C67CF9S4VU"
|
||||
telegram_app_center_id = "0"
|
||||
telegram_is_internal_build = "false"
|
||||
telegram_is_appstore_build = "true"
|
||||
telegram_appstore_id = "686449807"
|
||||
telegram_app_specific_url_scheme = "tg"
|
||||
telegram_aps_environment = "production"
|
7
build-system/example-generate-configuration
Executable file
7
build-system/example-generate-configuration
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! -d "$1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -R build-system/example-configuration/* "$1/"
|
@ -1,129 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
FASTLANE="$(which fastlane)"
|
||||
|
||||
EXPECTED_VARIABLES=(\
|
||||
APPLE_ID \
|
||||
BASE_BUNDLE_ID \
|
||||
APP_NAME \
|
||||
TEAM_ID \
|
||||
PROVISIONING_DIRECTORY \
|
||||
)
|
||||
|
||||
MISSING_VARIABLES="0"
|
||||
for VARIABLE_NAME in ${EXPECTED_VARIABLES[@]}; do
|
||||
if [ "${!VARIABLE_NAME}" = "" ]; then
|
||||
echo "$VARIABLE_NAME not defined"
|
||||
MISSING_VARIABLES="1"
|
||||
fi
|
||||
done
|
||||
if [ "$MISSING_VARIABLES" == "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROVISIONING_DIRECTORY" ]; then
|
||||
echo "Directory $PROVISIONING_DIRECTORY does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_DIR=$(mktemp -d)
|
||||
FASTLANE_DIR="$BASE_DIR/fastlane"
|
||||
mkdir "$FASTLANE_DIR"
|
||||
FASTFILE="$FASTLANE_DIR/Fastfile"
|
||||
|
||||
touch "$FASTFILE"
|
||||
|
||||
CREDENTIALS=(\
|
||||
--username "$APPLE_ID" \
|
||||
--team_id "$TEAM_ID" \
|
||||
)
|
||||
export FASTLANE_SKIP_UPDATE_CHECK=1
|
||||
|
||||
APP_EXTENSIONS=(\
|
||||
Share \
|
||||
SiriIntents \
|
||||
NotificationContent \
|
||||
NotificationService \
|
||||
Widget \
|
||||
)
|
||||
|
||||
echo "lane :manage_app do" >> "$FASTFILE"
|
||||
echo " produce(" >> "$FASTFILE"
|
||||
echo " username: '$APPLE_ID'," >> "$FASTFILE"
|
||||
echo " app_identifier: '${BASE_BUNDLE_ID}'," >> "$FASTFILE"
|
||||
echo " app_name: '$APP_NAME'," >> "$FASTFILE"
|
||||
echo " language: 'English'," >> "$FASTFILE"
|
||||
echo " app_version: '1.0'," >> "$FASTFILE"
|
||||
echo " team_id: '$TEAM_ID'," >> "$FASTFILE"
|
||||
echo " skip_itc: true," >> "$FASTFILE"
|
||||
echo " )" >> "$FASTFILE"
|
||||
|
||||
echo " produce(" >> "$FASTFILE"
|
||||
echo " username: '$APPLE_ID'," >> "$FASTFILE"
|
||||
echo " app_identifier: '${BASE_BUNDLE_ID}.watchkitapp'," >> "$FASTFILE"
|
||||
echo " app_name: '$APP_NAME Watch App'," >> "$FASTFILE"
|
||||
echo " language: 'English'," >> "$FASTFILE"
|
||||
echo " app_version: '1.0'," >> "$FASTFILE"
|
||||
echo " team_id: '$TEAM_ID'," >> "$FASTFILE"
|
||||
echo " skip_itc: true," >> "$FASTFILE"
|
||||
echo " )" >> "$FASTFILE"
|
||||
|
||||
echo " produce(" >> "$FASTFILE"
|
||||
echo " username: '$APPLE_ID'," >> "$FASTFILE"
|
||||
echo " app_identifier: '${BASE_BUNDLE_ID}.watchkitapp.watchkitextension'," >> "$FASTFILE"
|
||||
echo " app_name: '$APP_NAME Watch App Extension'," >> "$FASTFILE"
|
||||
echo " language: 'English'," >> "$FASTFILE"
|
||||
echo " app_version: '1.0'," >> "$FASTFILE"
|
||||
echo " team_id: '$TEAM_ID'," >> "$FASTFILE"
|
||||
echo " skip_itc: true," >> "$FASTFILE"
|
||||
echo " )" >> "$FASTFILE"
|
||||
|
||||
for EXTENSION in ${APP_EXTENSIONS[@]}; do
|
||||
echo " produce(" >> "$FASTFILE"
|
||||
echo " username: '$APPLE_ID'," >> "$FASTFILE"
|
||||
echo " app_identifier: '${BASE_BUNDLE_ID}.${EXTENSION}'," >> "$FASTFILE"
|
||||
echo " app_name: '${APP_NAME} ${EXTENSION}'," >> "$FASTFILE"
|
||||
echo " language: 'English'," >> "$FASTFILE"
|
||||
echo " app_version: '1.0'," >> "$FASTFILE"
|
||||
echo " team_id: '$TEAM_ID'," >> "$FASTFILE"
|
||||
echo " skip_itc: true," >> "$FASTFILE"
|
||||
echo " )" >> "$FASTFILE"
|
||||
done
|
||||
|
||||
echo "end" >> "$FASTFILE"
|
||||
|
||||
pushd "$BASE_DIR"
|
||||
|
||||
fastlane cert ${CREDENTIALS[@]} --development
|
||||
|
||||
fastlane manage_app
|
||||
|
||||
fastlane produce group -g "group.$BASE_BUNDLE_ID" -n "$APP_NAME Group" ${CREDENTIALS[@]}
|
||||
|
||||
fastlane produce enable_services -a "$BASE_BUNDLE_ID" ${CREDENTIALS[@]} \
|
||||
--app-group \
|
||||
--push-notification \
|
||||
--sirikit
|
||||
|
||||
fastlane produce associate_group -a "$BASE_BUNDLE_ID" "group.$BASE_BUNDLE_ID" ${CREDENTIALS[@]}
|
||||
for EXTENSION in ${APP_EXTENSIONS[@]}; do
|
||||
fastlane produce enable_services -a "${BASE_BUNDLE_ID}.${EXTENSION}" ${CREDENTIALS[@]} \
|
||||
--app-group
|
||||
|
||||
fastlane produce associate_group -a "${BASE_BUNDLE_ID}.${EXTENSION}" "group.$BASE_BUNDLE_ID" ${CREDENTIALS[@]}
|
||||
done
|
||||
|
||||
for DEVELOPMENT_FLAG in "--development"; do
|
||||
fastlane sigh -a "$BASE_BUNDLE_ID" ${CREDENTIALS[@]} -o "$PROVISIONING_DIRECTORY" $DEVELOPMENT_FLAG \
|
||||
--skip_install
|
||||
for EXTENSION in ${APP_EXTENSIONS[@]}; do
|
||||
fastlane sigh -a "${BASE_BUNDLE_ID}.${EXTENSION}" ${CREDENTIALS[@]} -o "$PROVISIONING_DIRECTORY" $DEVELOPMENT_FLAG \
|
||||
--skip_install
|
||||
done
|
||||
done
|
||||
|
||||
popd
|
||||
|
||||
rm -rf "$BASE_DIR"
|
@ -1 +0,0 @@
|
||||
12.2
|
@ -5,7 +5,7 @@ set -e
|
||||
BUILD_TELEGRAM_VERSION="1"
|
||||
|
||||
MACOS_VERSION="10.15"
|
||||
XCODE_VERSION="12.2"
|
||||
XCODE_VERSION="12.3"
|
||||
GUEST_SHELL="bash"
|
||||
|
||||
VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)"
|
||||
@ -58,13 +58,11 @@ cp "$BAZEL" "tools/bazel"
|
||||
BUILD_CONFIGURATION="$1"
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ]; then
|
||||
CODESIGNING_SUBPATH="transient-data/codesigning"
|
||||
CODESIGNING_TEAMS_SUBPATH="transient-data/teams"
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
CODESIGNING_SUBPATH="transient-data/codesigning"
|
||||
CODESIGNING_TEAMS_SUBPATH="transient-data/teams"
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
CODESIGNING_SUBPATH="fake-codesigning"
|
||||
CODESIGNING_SUBPATH="build-system/fake-codesigning"
|
||||
else
|
||||
echo "Unknown configuration $1"
|
||||
exit 1
|
||||
@ -90,46 +88,38 @@ fi
|
||||
BASE_DIR=$(pwd)
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
if [ ! `which setup-telegram-build.sh` ]; then
|
||||
echo "setup-telegram-build.sh not found in PATH $PATH"
|
||||
if [ ! `which generate-configuration.sh` ]; then
|
||||
echo "generate-configuration.sh not found in PATH $PATH"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! `which setup-codesigning.sh` ]; then
|
||||
echo "setup-codesigning.sh not found in PATH $PATH"
|
||||
exit 1
|
||||
fi
|
||||
source `which setup-telegram-build.sh`
|
||||
setup_telegram_build "$BUILD_CONFIGURATION" "$BASE_DIR/$BUILDBOX_DIR/transient-data"
|
||||
source `which setup-codesigning.sh`
|
||||
|
||||
CODESIGNING_CONFIGURATION="$BUILD_CONFIGURATION"
|
||||
if [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ]; then
|
||||
CODESIGNING_CONFIGURATION="hockeyapp"
|
||||
fi
|
||||
|
||||
setup_codesigning "$CODESIGNING_CONFIGURATION" "$BASE_DIR/$BUILDBOX_DIR/transient-data"
|
||||
if [ "$SETUP_TELEGRAM_BUILD_VERSION" != "$BUILD_TELEGRAM_VERSION" ]; then
|
||||
echo "setup-telegram-build.sh script version doesn't match"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
if [ -z "$TELEGRAM_BUILD_APPSTORE_PASSWORD" ]; then
|
||||
echo "TELEGRAM_BUILD_APPSTORE_PASSWORD is not set"
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
|
||||
case "$BUILD_CONFIGURATION" in
|
||||
"hockeyapp"|"appcenter-experimental"|"appcenter-experimental-2")
|
||||
generate-configuration.sh internal release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
"appstore")
|
||||
generate-configuration.sh appstore release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build configuration $BUILD_CONFIGURATION"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BUILD_APPSTORE_TEAM_NAME" ]; then
|
||||
echo "TELEGRAM_BUILD_APPSTORE_TEAM_NAME is not set"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BUILD_APPSTORE_USERNAME" ]; then
|
||||
echo "TELEGRAM_BUILD_APPSTORE_USERNAME is not set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
|
||||
cp -R build-system/fake-codesigning/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning/"
|
||||
cp -R build-system/example-configuration/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration/"
|
||||
fi
|
||||
|
||||
if [ ! -d "$BUILDBOX_DIR/$CODESIGNING_SUBPATH" ]; then
|
||||
echo "$BUILDBOX_DIR/$CODESIGNING_SUBPATH does not exist"
|
||||
if [ ! -d "$CODESIGNING_SUBPATH" ]; then
|
||||
echo "$CODESIGNING_SUBPATH does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -185,17 +175,12 @@ elif [ "$BUILD_MACHINE" == "macOS" ]; then
|
||||
echo "VM_IP=$VM_IP"
|
||||
fi
|
||||
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/$CODESIGNING_SUBPATH" telegram@"$VM_IP":codesigning_data
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/$CODESIGNING_TEAMS_SUBPATH" telegram@"$VM_IP":codesigning_teams
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$CODESIGNING_SUBPATH" telegram@"$VM_IP":codesigning_data
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration" telegram@"$VM_IP":telegram-configuration
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "mkdir -p telegram-ios-shared/fastlane; echo '' > telegram-ios-shared/fastlane/Fastfile"
|
||||
else
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/transient-data/telegram-ios-shared" telegram@"$VM_IP":telegram-ios-shared
|
||||
fi
|
||||
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/guest-build-telegram.sh" "$BUILDBOX_DIR/transient-data/source.tar" telegram@"$VM_IP":
|
||||
|
||||
ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "export TELEGRAM_BUILD_APPSTORE_PASSWORD=\"$TELEGRAM_BUILD_APPSTORE_PASSWORD\"; export TELEGRAM_BUILD_APPSTORE_TEAM_NAME=\"$TELEGRAM_BUILD_APPSTORE_TEAM_NAME\"; export TELEGRAM_BUILD_APPSTORE_USERNAME=\"$TELEGRAM_BUILD_APPSTORE_USERNAME\"; export BUILD_NUMBER=\"$BUILD_NUMBER\"; export COMMIT_ID=\"$COMMIT_ID\"; export COMMIT_AUTHOR=\"$COMMIT_AUTHOR\"; export BAZEL_HTTP_CACHE_URL=\"$BAZEL_HTTP_CACHE_URL\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true
|
||||
ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "export BUILD_NUMBER=\"$BUILD_NUMBER\"; export BAZEL_HTTP_CACHE_URL=\"$BAZEL_HTTP_CACHE_URL\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true
|
||||
|
||||
OUTPUT_PATH="build/artifacts"
|
||||
rm -rf "$OUTPUT_PATH"
|
||||
|
@ -7,31 +7,14 @@ if [ -z "BUILD_NUMBER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "COMMIT_ID" ]; then
|
||||
echo "COMMIT_ID is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" == "hockeyapp" ] || [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ] || [ "$1" == "testinghockeyapp" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs"
|
||||
PROFILES_PATH="$HOME/codesigning_data/profiles"
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/enterprise"
|
||||
elif [ "$1" == "testinghockeyapp-local" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs"
|
||||
PROFILES_PATH="$HOME/codesigning_data/profiles"
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/enterprise"
|
||||
elif [ "$1" == "appstore" ]; then
|
||||
if [ -z "$TELEGRAM_BUILD_APPSTORE_PASSWORD" ]; then
|
||||
echo "TELEGRAM_BUILD_APPSTORE_PASSWORD is not set"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BUILD_APPSTORE_TEAM_NAME" ]; then
|
||||
echo "TELEGRAM_BUILD_APPSTORE_TEAM_NAME is not set"
|
||||
exit 1
|
||||
fi
|
||||
CERTS_PATH="$HOME/codesigning_data/certs"
|
||||
PROFILES_PATH="$HOME/codesigning_data/profiles"
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/distribution"
|
||||
elif [ "$1" == "verify" ]; then
|
||||
CERTS_PATH="build-system/fake-codesigning/certs/distribution"
|
||||
PROFILES_PATH="build-system/fake-codesigning/profiles"
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/distribution"
|
||||
else
|
||||
echo "Unknown configuration $1"
|
||||
exit 1
|
||||
@ -79,7 +62,7 @@ echo "Unpacking files..."
|
||||
|
||||
mkdir -p "$SOURCE_PATH/buildbox"
|
||||
mkdir -p "$SOURCE_PATH/buildbox/transient-data"
|
||||
cp -r "$HOME/codesigning_teams" "$SOURCE_PATH/buildbox/transient-data/teams"
|
||||
#cp -r "$HOME/codesigning_teams" "$SOURCE_PATH/buildbox/transient-data/teams"
|
||||
|
||||
BASE_DIR=$(pwd)
|
||||
cd "$SOURCE_PATH"
|
||||
@ -95,38 +78,24 @@ done
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN"
|
||||
|
||||
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
|
||||
for f in $(ls "$PROFILES_PATH"); do
|
||||
PROFILE_PATH="$PROFILES_PATH/$f"
|
||||
uuid=`grep UUID -A1 -a "$PROFILE_PATH" | grep -io "[-A-F0-9]\{36\}"`
|
||||
cp -f "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$uuid.mobileprovision"
|
||||
done
|
||||
|
||||
if [ "$1" == "hockeyapp" ] || [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ]; then
|
||||
BUILD_ENV_SCRIPT="../telegram-ios-shared/buildbox/bin/internal.sh"
|
||||
APP_TARGET="bazel_app_arm64"
|
||||
APP_CONFIGURATION="release_arm64"
|
||||
elif [ "$1" == "appstore" ]; then
|
||||
BUILD_ENV_SCRIPT="../telegram-ios-shared/buildbox/bin/appstore.sh"
|
||||
APP_TARGET="bazel_app"
|
||||
APP_CONFIGURATION="release_universal"
|
||||
elif [ "$1" == "verify" ]; then
|
||||
BUILD_ENV_SCRIPT="build-system/verify.sh"
|
||||
APP_TARGET="bazel_app"
|
||||
export CODESIGNING_DATA_PATH="build-system/fake-codesigning"
|
||||
export CODESIGNING_CERTS_VARIANT="distribution"
|
||||
export CODESIGNING_PROFILES_VARIANT="appstore"
|
||||
APP_CONFIGURATION="release_universal"
|
||||
else
|
||||
echo "Unsupported configuration $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" == "appcenter-experimental" ]; then
|
||||
export APP_CENTER_ID="$APP_CENTER_EXPERIMENTAL_ID"
|
||||
elif [ "$1" == "appcenter-experimental-2" ]; then
|
||||
export APP_CENTER_ID="$APP_CENTER_EXPERIMENTAL_2_ID"
|
||||
fi
|
||||
|
||||
PATH="$PATH:$(pwd)/tools" BAZEL_HTTP_CACHE_URL="$BAZEL_HTTP_CACHE_URL" LOCAL_CODESIGNING=1 sh "$BUILD_ENV_SCRIPT" make "$APP_TARGET"
|
||||
python3 build-system/Make/Make.py \
|
||||
--bazel="$(pwd)/tools/bazel" \
|
||||
--cacheHost="$BAZEL_HTTP_CACHE_URL" \
|
||||
build \
|
||||
--configurationPath="$HOME/telegram-configuration" \
|
||||
--buildNumber="$BUILD_NUMBER" \
|
||||
--configuration="$APP_CONFIGURATION"
|
||||
|
||||
OUTPUT_PATH="build/artifacts"
|
||||
rm -rf "$OUTPUT_PATH"
|
||||
|
@ -202,19 +202,29 @@ public final class ChatPeekTimeout {
|
||||
|
||||
public final class ChatPeerNearbyData: Equatable {
|
||||
public static func == (lhs: ChatPeerNearbyData, rhs: ChatPeerNearbyData) -> Bool {
|
||||
return lhs.distance == rhs.distance
|
||||
}
|
||||
|
||||
public let distance: Int32
|
||||
|
||||
public init(distance: Int32) {
|
||||
self.distance = distance
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatGreetingData: Equatable {
|
||||
public static func == (lhs: ChatGreetingData, rhs: ChatGreetingData) -> Bool {
|
||||
if let lhsSticker = lhs.sticker, let rhsSticker = rhs.sticker, !lhsSticker.isEqual(to: rhsSticker) {
|
||||
return false
|
||||
} else if (lhs.sticker == nil) != (rhs.sticker == nil) {
|
||||
return false
|
||||
}
|
||||
return lhs.distance == rhs.distance
|
||||
return true
|
||||
}
|
||||
|
||||
public let distance: Int32
|
||||
public let sticker: TelegramMediaFile?
|
||||
|
||||
public init(distance: Int32, sticker: TelegramMediaFile?) {
|
||||
self.distance = distance
|
||||
public init(sticker: TelegramMediaFile?) {
|
||||
self.sticker = sticker
|
||||
}
|
||||
}
|
||||
@ -270,12 +280,13 @@ public final class NavigateToChatControllerParams {
|
||||
public let activateMessageSearch: (ChatSearchDomain, String)?
|
||||
public let peekData: ChatPeekTimeout?
|
||||
public let peerNearbyData: ChatPeerNearbyData?
|
||||
public let greetingData: ChatGreetingData?
|
||||
public let animated: Bool
|
||||
public let options: NavigationAnimationOptions
|
||||
public let parentGroupId: PeerGroupId?
|
||||
public let completion: (ChatController) -> Void
|
||||
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
self.navigationController = navigationController
|
||||
self.chatController = chatController
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
@ -292,6 +303,7 @@ public final class NavigateToChatControllerParams {
|
||||
self.activateMessageSearch = activateMessageSearch
|
||||
self.peekData = peekData
|
||||
self.peerNearbyData = peerNearbyData
|
||||
self.greetingData = greetingData
|
||||
self.animated = animated
|
||||
self.options = options
|
||||
self.parentGroupId = parentGroupId
|
||||
@ -383,11 +395,13 @@ public struct ContactListAdditionalOption: Equatable {
|
||||
public let title: String
|
||||
public let icon: ContactListActionItemIcon
|
||||
public let action: () -> Void
|
||||
public let clearHighlightAutomatically: Bool
|
||||
|
||||
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void) {
|
||||
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
self.clearHighlightAutomatically = clearHighlightAutomatically
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
|
||||
|
@ -6,14 +6,21 @@ import Postbox
|
||||
import TelegramCore
|
||||
|
||||
public struct ChatListNodeAdditionalCategory {
|
||||
public enum Appearance {
|
||||
case option
|
||||
case action
|
||||
}
|
||||
|
||||
public var id: Int
|
||||
public var icon: UIImage?
|
||||
public var title: String
|
||||
public var appearance: Appearance
|
||||
|
||||
public init(id: Int, icon: UIImage?, title: String) {
|
||||
public init(id: Int, icon: UIImage?, title: String, appearance: Appearance = .option) {
|
||||
self.id = id
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.appearance = appearance
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,20 +32,29 @@ public struct ChatListNodePeersFilter: OptionSet {
|
||||
public final class PeerSelectionControllerParams {
|
||||
public let context: AccountContext
|
||||
public let filter: ChatListNodePeersFilter
|
||||
public let hasChatListSelector: Bool
|
||||
public let hasContactSelector: Bool
|
||||
public let hasGlobalSearch: Bool
|
||||
public let title: String?
|
||||
public let attemptSelection: ((Peer) -> Void)?
|
||||
public let createNewGroup: (() -> Void)?
|
||||
public let pretendPresentedInModal: Bool
|
||||
|
||||
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil) {
|
||||
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) {
|
||||
self.context = context
|
||||
self.filter = filter
|
||||
self.hasChatListSelector = hasChatListSelector
|
||||
self.hasContactSelector = hasContactSelector
|
||||
self.hasGlobalSearch = hasGlobalSearch
|
||||
self.title = title
|
||||
self.attemptSelection = attemptSelection
|
||||
self.createNewGroup = createNewGroup
|
||||
self.pretendPresentedInModal = pretendPresentedInModal
|
||||
}
|
||||
}
|
||||
|
||||
public protocol PeerSelectionController: ViewController {
|
||||
var peerSelected: ((PeerId) -> Void)? { get set }
|
||||
var peerSelected: ((Peer) -> Void)? { get set }
|
||||
var inProgress: Bool { get set }
|
||||
var customDismiss: (() -> Void)? { get set }
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ public protocol PresentationGroupCall: class {
|
||||
func setFullSizeVideo(peerId: PeerId?)
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
|
||||
func updateMuteState(peerId: PeerId, isMuted: Bool)
|
||||
func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
|
||||
|
||||
func invitePeer(_ peerId: PeerId) -> Bool
|
||||
func removedPeer(_ peerId: PeerId)
|
||||
|
@ -72,8 +72,9 @@ public final class AnimatedStickerFrame {
|
||||
public let bytesPerRow: Int
|
||||
let index: Int
|
||||
let isLastFrame: Bool
|
||||
let totalFrames: Int
|
||||
|
||||
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool) {
|
||||
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool, totalFrames: Int) {
|
||||
self.data = data
|
||||
self.type = type
|
||||
self.width = width
|
||||
@ -81,6 +82,7 @@ public final class AnimatedStickerFrame {
|
||||
self.bytesPerRow = bytesPerRow
|
||||
self.index = index
|
||||
self.isLastFrame = isLastFrame
|
||||
self.totalFrames = totalFrames
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +257,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
||||
}
|
||||
|
||||
if let frameData = frameData, draw {
|
||||
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
|
||||
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -633,7 +635,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
|
||||
} else {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
@ -643,7 +645,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
||||
if let cache = self.cache {
|
||||
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
@ -744,6 +746,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
private var reportedStarted = false
|
||||
|
||||
public var completed: (Bool) -> Void = { _ in }
|
||||
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
|
||||
|
||||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
||||
@ -757,6 +760,8 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
private var canDisplayFirstFrame: Bool = false
|
||||
private var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||
|
||||
public var stopAtNearestLoop: Bool = false
|
||||
|
||||
private let playbackStatus = Promise<AnimatedStickerStatus>()
|
||||
public var status: Signal<AnimatedStickerStatus, NoError> {
|
||||
return self.playbackStatus.get()
|
||||
@ -964,9 +969,17 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
||||
|
||||
if frame.isLastFrame {
|
||||
var stopped = false
|
||||
var stopNow = false
|
||||
if case .once = strongSelf.playbackMode {
|
||||
stopNow = true
|
||||
} else if strongSelf.stopAtNearestLoop {
|
||||
stopNow = true
|
||||
}
|
||||
if stopNow {
|
||||
strongSelf.stop()
|
||||
strongSelf.isPlaying = false
|
||||
stopped = true
|
||||
@ -1041,9 +1054,17 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
||||
|
||||
if frame.isLastFrame {
|
||||
var stopped = false
|
||||
var stopNow = false
|
||||
if case .once = strongSelf.playbackMode {
|
||||
stopNow = true
|
||||
} else if strongSelf.stopAtNearestLoop {
|
||||
stopNow = true
|
||||
}
|
||||
if stopNow {
|
||||
strongSelf.stop()
|
||||
strongSelf.isPlaying = false
|
||||
stopped = true
|
||||
|
@ -13,6 +13,7 @@ import AccountContext
|
||||
import Emoji
|
||||
|
||||
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
|
||||
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
|
||||
private let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
|
||||
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
|
||||
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
|
||||
@ -79,10 +80,14 @@ private let savedMessagesColors: NSArray = [
|
||||
UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor
|
||||
]
|
||||
|
||||
public enum AvatarNodeExplicitIcon {
|
||||
case phone
|
||||
}
|
||||
|
||||
private enum AvatarNodeState: Equatable {
|
||||
case empty
|
||||
case peerAvatar(PeerId, [String], TelegramMediaImageRepresentation?)
|
||||
case custom(letter: [String], explicitColorIndex: Int?)
|
||||
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
|
||||
}
|
||||
|
||||
private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
||||
@ -91,8 +96,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
||||
return true
|
||||
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)):
|
||||
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations
|
||||
case let (.custom(lhsLetters, lhsIndex), .custom(rhsLetters, rhsIndex)):
|
||||
return lhsLetters == rhsLetters && lhsIndex == rhsIndex
|
||||
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
|
||||
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -105,6 +110,7 @@ private enum AvatarNodeIcon: Equatable {
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeImageOverride: Equatable {
|
||||
@ -115,6 +121,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeColorOverride {
|
||||
@ -323,6 +330,9 @@ public final class AvatarNode: ASDisplayNode {
|
||||
case .deletedIcon:
|
||||
representation = nil
|
||||
icon = .deletedIcon
|
||||
case .phoneIcon:
|
||||
representation = nil
|
||||
icon = .phoneIcon
|
||||
}
|
||||
} else if peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil {
|
||||
representation = peer?.smallProfileImage
|
||||
@ -383,7 +393,7 @@ public final class AvatarNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil) {
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
|
||||
var explicitIndex: Int?
|
||||
if let explicitColor = explicitColor {
|
||||
switch explicitColor {
|
||||
@ -391,11 +401,16 @@ public final class AvatarNode: ASDisplayNode {
|
||||
explicitIndex = 5
|
||||
}
|
||||
}
|
||||
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex)
|
||||
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
|
||||
if updatedState != self.state {
|
||||
self.state = updatedState
|
||||
|
||||
let parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
let parameters: AvatarNodeParameters
|
||||
if let icon = icon, case .phone = icon {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
} else {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
}
|
||||
|
||||
self.displaySuspended = true
|
||||
self.contents = nil
|
||||
@ -456,6 +471,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
|
||||
if case .deletedIcon = parameters.icon {
|
||||
colorsArray = grayscaleColors
|
||||
} else if case .phoneIcon = parameters.icon {
|
||||
colorsArray = grayscaleColors
|
||||
} else if case .savedMessagesIcon = parameters.icon {
|
||||
colorsArray = savedMessagesColors
|
||||
} else if case .repliesIcon = parameters.icon {
|
||||
@ -505,6 +522,15 @@ public final class AvatarNode: ASDisplayNode {
|
||||
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))
|
||||
}
|
||||
} else if case .phoneIcon = parameters.icon {
|
||||
let factor: CGFloat = 1.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 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))
|
||||
}
|
||||
} else if case .savedMessagesIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
|
@ -1,5 +1,5 @@
|
||||
load(
|
||||
"//build-input/data:variables.bzl",
|
||||
"@build_configuration//:variables.bzl",
|
||||
"telegram_api_id",
|
||||
"telegram_api_hash",
|
||||
"telegram_app_center_id",
|
||||
|
@ -146,7 +146,7 @@ API_AVAILABLE(ios(10))
|
||||
dataDict[@"device_token"] = [appToken base64EncodedStringWithOptions:0];
|
||||
dataDict[@"device_token_type"] = @"voip";
|
||||
}
|
||||
float tzOffset = ([[NSTimeZone systemTimeZone] secondsFromGMT] / 3600.0);
|
||||
float tzOffset = [[NSTimeZone systemTimeZone] secondsFromGMT];
|
||||
dataDict[@"tz_offset"] = @((int)tzOffset);
|
||||
if (signatureDict != nil) {
|
||||
for (id<NSCopying> key in signatureDict.allKeys) {
|
||||
|
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
|
||||
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,12 +13,58 @@ import AccountContext
|
||||
import AlertUI
|
||||
import AppBundle
|
||||
import LocalizedPeerData
|
||||
import ContextUI
|
||||
|
||||
public enum CallListControllerMode {
|
||||
case tab
|
||||
case navigation
|
||||
}
|
||||
|
||||
private final class DeleteAllButtonNode: ASDisplayNode {
|
||||
private let pressed: () -> Void
|
||||
|
||||
let contentNode: ContextExtractedContentContainingNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
init(presentationData: PresentationData, pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
self.contentNode = ContextExtractedContentContainingNode()
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
self.buttonNode.addSubnode(self.titleNode)
|
||||
self.contentNode.contentNode.addSubnode(self.buttonNode)
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.Notification_Exceptions_DeleteAll, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationBar.accentTextColor)
|
||||
|
||||
//self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let titleSize = self.titleNode.updateLayout(constrainedSize)
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(), size: titleSize)
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(), size: titleSize)
|
||||
return titleSize
|
||||
}
|
||||
|
||||
override public func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.contentNode.contentRect = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
public final class CallListController: ViewController {
|
||||
private var controllerNode: CallListControllerNode {
|
||||
return self.displayNode as! CallListControllerNode
|
||||
@ -43,6 +89,7 @@ public final class CallListController: ViewController {
|
||||
private var editingMode: Bool = false
|
||||
|
||||
private let createActionDisposable = MetaDisposable()
|
||||
private let clearDisposable = MetaDisposable()
|
||||
|
||||
public init(context: AccountContext, mode: CallListControllerMode) {
|
||||
self.context = context
|
||||
@ -104,6 +151,7 @@ public final class CallListController: ViewController {
|
||||
self.createActionDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.peerViewDisposable.dispose()
|
||||
self.clearDisposable.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
@ -167,6 +215,7 @@ public final class CallListController: ViewController {
|
||||
switch strongSelf.mode {
|
||||
case .tab:
|
||||
strongSelf.navigationItem.setLeftBarButton(nil, animated: true)
|
||||
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
|
||||
case .navigation:
|
||||
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
|
||||
}
|
||||
@ -175,8 +224,25 @@ public final class CallListController: ViewController {
|
||||
case .tab:
|
||||
if strongSelf.editingMode {
|
||||
strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed))
|
||||
var pressedImpl: (() -> Void)?
|
||||
let buttonNode = DeleteAllButtonNode(presentationData: strongSelf.presentationData, pressed: {
|
||||
pressedImpl?()
|
||||
})
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode)
|
||||
strongSelf.navigationItem.rightBarButtonItem?.setCustomAction({
|
||||
pressedImpl?()
|
||||
})
|
||||
pressedImpl = { [weak self, weak buttonNode] in
|
||||
guard let strongSelf = self, let buttonNode = buttonNode else {
|
||||
return
|
||||
}
|
||||
strongSelf.deleteAllPressed(buttonNode: buttonNode)
|
||||
}
|
||||
|
||||
//strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: strongSelf, action: #selector(strongSelf.deleteAllPressed))
|
||||
} else {
|
||||
strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed))
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.callPressed))
|
||||
}
|
||||
case .navigation:
|
||||
if strongSelf.editingMode {
|
||||
@ -203,6 +269,89 @@ public final class CallListController: ViewController {
|
||||
self.beginCallImpl()
|
||||
}
|
||||
|
||||
@objc private func deleteAllPressed(buttonNode: DeleteAllButtonNode) {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
let beginClear: (Bool) -> Void = { [weak self] forEveryone in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var signal = clearCallHistory(account: strongSelf.context.account, forEveryone: forEveryone)
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
signal = signal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
self?.clearDisposable.set(nil)
|
||||
}
|
||||
strongSelf.clearDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
}))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForMe, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
beginClear(false)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForEveryone, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
beginClear(true)
|
||||
})))
|
||||
|
||||
final class ExtractedContentSourceImpl: ContextExtractedContentSource {
|
||||
var keepInPlace: Bool
|
||||
let ignoreContentTouches: Bool = true
|
||||
let blurBackground: Bool
|
||||
|
||||
private let controller: ViewController
|
||||
private let sourceNode: ContextExtractedContentContainingNode
|
||||
|
||||
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.keepInPlace = keepInPlace
|
||||
self.blurBackground = blurBackground
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(items), reactionItems: [], gesture: nil)
|
||||
self.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
private func beginCallImpl() {
|
||||
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }, displayCallIcons: true))
|
||||
controller.navigationPresentation = .modal
|
||||
@ -234,9 +383,25 @@ public final class CallListController: ViewController {
|
||||
|
||||
@objc func editPressed() {
|
||||
self.editingMode = true
|
||||
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
var pressedImpl: (() -> Void)?
|
||||
let buttonNode = DeleteAllButtonNode(presentationData: self.presentationData, pressed: {
|
||||
pressedImpl?()
|
||||
})
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode)
|
||||
self.navigationItem.rightBarButtonItem?.setCustomAction({
|
||||
pressedImpl?()
|
||||
})
|
||||
pressedImpl = { [weak self, weak buttonNode] in
|
||||
guard let strongSelf = self, let buttonNode = buttonNode else {
|
||||
return
|
||||
}
|
||||
strongSelf.deleteAllPressed(buttonNode: buttonNode)
|
||||
}
|
||||
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.deleteAllPressed))
|
||||
case .navigation:
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
}
|
||||
@ -251,6 +416,7 @@ public final class CallListController: ViewController {
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
|
||||
case .navigation:
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
}
|
||||
|
@ -120,8 +120,8 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(_, text):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, editing, isActive):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, _, isActive):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
@ -139,8 +139,8 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(_, text):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, editing, isActive):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, _, isActive):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
@ -263,9 +263,49 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
self?.openInfo(peerId, messages)
|
||||
}, delete: { [weak self] messageIds in
|
||||
if let strongSelf = self {
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
guard let strongSelf = self, let peerId = messageIds.first?.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forEveryone).start()
|
||||
}))
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
}, updateShowCallsTab: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, {
|
||||
|
@ -432,7 +432,7 @@ class CallListGroupCallItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateFrameAdditive(node: strongSelf.joinBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size))
|
||||
|
||||
let _ = joinTitleApply()
|
||||
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size))
|
||||
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: joinTitleLayout.size))
|
||||
|
||||
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
|
||||
|
@ -226,7 +226,10 @@ func callListNodeEntriesForView(view: CallListView, groupCalls: [Peer], state: C
|
||||
func countMeaningfulCallListEntries(_ entries: [CallListNodeEntry]) -> Int {
|
||||
var count: Int = 0
|
||||
for entry in entries {
|
||||
if case .setting = entry.stableId {} else {
|
||||
switch entry.stableId {
|
||||
case .setting, .groupCall:
|
||||
break
|
||||
default:
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
227
submodules/CameraUI/Sources/CameraZoomNode.swift
Normal file
227
submodules/CameraUI/Sources/CameraZoomNode.swift
Normal file
@ -0,0 +1,227 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private final class ZoomWheelNodeDrawingState: NSObject {
|
||||
let transition: CGFloat
|
||||
let reverse: Bool
|
||||
|
||||
init(transition: CGFloat, reverse: Bool) {
|
||||
self.transition = transition
|
||||
self.reverse = reverse
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
final class ZoomWheelNode: ASDisplayNode {
|
||||
class State: Equatable {
|
||||
let active: Bool
|
||||
|
||||
init(active: Bool) {
|
||||
self.active = active
|
||||
}
|
||||
|
||||
static func ==(lhs: State, rhs: State) -> Bool {
|
||||
if lhs.active != rhs.active {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class TransitionContext {
|
||||
let startTime: Double
|
||||
let duration: Double
|
||||
let previousState: State
|
||||
|
||||
init(startTime: Double, duration: Double, previousState: State) {
|
||||
self.startTime = startTime
|
||||
self.duration = duration
|
||||
self.previousState = previousState
|
||||
}
|
||||
}
|
||||
|
||||
private var animator: ConstantDisplayLinkAnimator?
|
||||
|
||||
private var hasState = false
|
||||
private var state: State = State(active: false)
|
||||
private var transitionContext: TransitionContext?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
func update(state: State, animated: Bool) {
|
||||
var animated = animated
|
||||
if !self.hasState {
|
||||
self.hasState = true
|
||||
animated = false
|
||||
}
|
||||
|
||||
if self.state != state {
|
||||
let previousState = self.state
|
||||
self.state = state
|
||||
|
||||
if animated {
|
||||
self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState)
|
||||
}
|
||||
|
||||
self.updateAnimations()
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAnimations() {
|
||||
var animate = false
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
if let transitionContext = self.transitionContext {
|
||||
if transitionContext.startTime + transitionContext.duration < timestamp {
|
||||
self.transitionContext = nil
|
||||
} else {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
|
||||
if animate {
|
||||
let animator: ConstantDisplayLinkAnimator
|
||||
if let current = self.animator {
|
||||
animator = current
|
||||
} else {
|
||||
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.updateAnimations()
|
||||
})
|
||||
self.animator = animator
|
||||
}
|
||||
animator.isPaused = false
|
||||
} else {
|
||||
self.animator?.isPaused = true
|
||||
}
|
||||
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
var transitionFraction: CGFloat = self.state.active ? 1.0 : 0.0
|
||||
|
||||
var reverse = false
|
||||
if let transitionContext = self.transitionContext {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
|
||||
t = min(1.0, max(0.0, t))
|
||||
|
||||
if transitionContext.previousState.active != self.state.active {
|
||||
transitionFraction = self.state.active ? t : 1.0 - t
|
||||
|
||||
reverse = transitionContext.previousState.active
|
||||
}
|
||||
}
|
||||
|
||||
return ZoomWheelNodeDrawingState(transition: transitionFraction, reverse: reverse)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
guard let parameters = parameters as? ZoomWheelNodeDrawingState else {
|
||||
return
|
||||
}
|
||||
|
||||
let color = UIColor(rgb: 0xffffff)
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
let clearLineWidth: CGFloat = 4.0
|
||||
let lineWidth: CGFloat = 1.0 + UIScreenPixel
|
||||
|
||||
context.scaleBy(x: 2.5, y: 2.5)
|
||||
|
||||
context.translateBy(x: 4.0, y: 3.0)
|
||||
let _ = try? drawSvgPath(context, path: "M14,8.335 C14.36727,8.335 14.665,8.632731 14.665,9 C14.665,11.903515 12.48064,14.296846 9.665603,14.626311 L9.665,16 C9.665,16.367269 9.367269,16.665 9,16.665 C8.666119,16.665 8.389708,16.418942 8.34221,16.098269 L8.335,16 L8.3354,14.626428 C5.519879,14.297415 3.335,11.90386 3.335,9 C3.335,8.632731 3.632731,8.335 4,8.335 C4.367269,8.335 4.665,8.632731 4.665,9 C4.665,11.394154 6.605846,13.335 9,13.335 C11.39415,13.335 13.335,11.394154 13.335,9 C13.335,8.632731 13.63273,8.335 14,8.335 Z ")
|
||||
|
||||
let _ = try? drawSvgPath(context, path: "M9,2.5 C10.38071,2.5 11.5,3.61929 11.5,5 L11.5,9 C11.5,10.380712 10.38071,11.5 9,11.5 C7.619288,11.5 6.5,10.380712 6.5,9 L6.5,5 C6.5,3.61929 7.619288,2.5 9,2.5 Z ")
|
||||
|
||||
context.translateBy(x: -4.0, y: -3.0)
|
||||
|
||||
if parameters.transition > 0.0 {
|
||||
let startPoint: CGPoint
|
||||
let endPoint: CGPoint
|
||||
|
||||
let origin = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel)
|
||||
let length: CGFloat = 17.0
|
||||
|
||||
if parameters.reverse {
|
||||
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
|
||||
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
|
||||
} else {
|
||||
startPoint = origin
|
||||
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
|
||||
}
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.setLineWidth(clearLineWidth)
|
||||
|
||||
context.move(to: startPoint)
|
||||
context.addLine(to: endPoint)
|
||||
context.strokePath()
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
|
||||
context.move(to: startPoint)
|
||||
context.addLine(to: endPoint)
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonNode: HighlightTrackingButtonNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.textNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlight in
|
||||
if let strongSelf = self {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final class CameraZoomNode: ASDisplayNode {
|
||||
private let wheelNode: ZoomWheelNode
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
override init() {
|
||||
self.wheelNode = ZoomWheelNode()
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.wheelNode)
|
||||
}
|
||||
}
|
18
submodules/ChatHistoryImportTasks/BUILD
Normal file
18
submodules/ChatHistoryImportTasks/BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatHistoryImportTasks",
|
||||
module_name = "ChatHistoryImportTasks",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ChatHistoryImportTasks {
|
||||
public final class Context {
|
||||
|
||||
}
|
||||
|
||||
public static func importState(peerId: PeerId) -> Signal<Float?, NoError> {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
32
submodules/ChatImportUI/BUILD
Normal file
32
submodules/ChatImportUI/BUILD
Normal file
@ -0,0 +1,32 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatImportUI",
|
||||
module_name = "ChatImportUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//third-party/ZipArchive:ZipArchive",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
|
||||
"//submodules/MimeTypes:MimeTypes",
|
||||
"//submodules/ConfettiEffect:ConfettiEffect",
|
||||
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
844
submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift
Normal file
844
submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift
Normal file
@ -0,0 +1,844 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import RadialStatusNode
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
import ZipArchive
|
||||
import MimeTypes
|
||||
import ConfettiEffect
|
||||
import TelegramUniversalVideoContent
|
||||
import SolidRoundedButtonNode
|
||||
|
||||
private final class ProgressEstimator {
|
||||
private var averageProgressPerSecond: Double = 0.0
|
||||
private var lastMeasurement: (Double, Float)?
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func update(progress: Float) -> Double? {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if let (lastTimestamp, lastProgress) = self.lastMeasurement {
|
||||
if abs(lastProgress - progress) >= 0.01 || abs(lastTimestamp - timestamp) > 1.0 {
|
||||
let immediateProgressPerSecond = Double(progress - lastProgress) / (timestamp - lastTimestamp)
|
||||
let alpha: Double = 0.01
|
||||
self.averageProgressPerSecond = alpha * immediateProgressPerSecond + (1.0 - alpha) * self.averageProgressPerSecond
|
||||
self.lastMeasurement = (timestamp, progress)
|
||||
}
|
||||
} else {
|
||||
self.lastMeasurement = (timestamp, progress)
|
||||
}
|
||||
|
||||
//print("progress = \(progress)")
|
||||
//print("averageProgressPerSecond = \(self.averageProgressPerSecond)")
|
||||
|
||||
if self.averageProgressPerSecond < 0.0001 {
|
||||
return nil
|
||||
} else {
|
||||
let remainingProgress = Double(1.0 - progress)
|
||||
let remainingTime = remainingProgress / self.averageProgressPerSecond
|
||||
//print("remainingTime \(remainingTime)")
|
||||
return remainingTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImportManager {
|
||||
enum ImportError {
|
||||
case generic
|
||||
case chatAdminRequired
|
||||
case invalidChatType
|
||||
case userBlocked
|
||||
}
|
||||
|
||||
enum State {
|
||||
case progress(totalBytes: Int, totalUploadedBytes: Int, totalMediaBytes: Int, totalUploadedMediaBytes: Int)
|
||||
case error(ImportError)
|
||||
case done
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private let archivePath: String?
|
||||
private let entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||
|
||||
private var session: ChatHistoryImport.Session?
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private let totalBytes: Int
|
||||
private let totalMediaBytes: Int
|
||||
private let mainFileSize: Int
|
||||
private var pendingEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||
private var entryProgress: [String: (Int, Int)] = [:]
|
||||
private var activeEntries: [String: Disposable] = [:]
|
||||
|
||||
private var stateValue: State {
|
||||
didSet {
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
}
|
||||
}
|
||||
private let statePromise = Promise<State>()
|
||||
var state: Signal<State, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
|
||||
self.account = account
|
||||
self.archivePath = archivePath
|
||||
self.entries = entries
|
||||
self.pendingEntries = entries
|
||||
|
||||
self.mainFileSize = fileSize(mainFile.path) ?? 0
|
||||
|
||||
var totalMediaBytes = 0
|
||||
for entry in self.entries {
|
||||
self.entryProgress[entry.0.path] = (Int(entry.0.uncompressedSize), 0)
|
||||
totalMediaBytes += Int(entry.0.uncompressedSize)
|
||||
}
|
||||
self.totalBytes = self.mainFileSize + totalMediaBytes
|
||||
self.totalMediaBytes = totalMediaBytes
|
||||
|
||||
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: 0, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: 0)
|
||||
|
||||
self.disposable.set((ChatHistoryImport.initSession(account: self.account, peerId: peerId, file: mainFile, mediaCount: Int32(entries.count))
|
||||
|> mapError { error -> ImportError in
|
||||
switch error {
|
||||
case .chatAdminRequired:
|
||||
return .chatAdminRequired
|
||||
case .invalidChatType:
|
||||
return .invalidChatType
|
||||
case .generic:
|
||||
return .generic
|
||||
case .userBlocked:
|
||||
return .userBlocked
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] session in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.session = session
|
||||
strongSelf.updateState()
|
||||
}, error: { [weak self] error in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.failWithError(error)
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
for (_, disposable) in self.activeEntries {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
if case .error = self.stateValue {
|
||||
return
|
||||
}
|
||||
|
||||
var totalUploadedMediaBytes = 0
|
||||
for (_, entrySizes) in self.entryProgress {
|
||||
totalUploadedMediaBytes += entrySizes.1
|
||||
}
|
||||
|
||||
var totalUploadedBytes = totalUploadedMediaBytes
|
||||
if let _ = self.session {
|
||||
totalUploadedBytes += self.mainFileSize
|
||||
}
|
||||
|
||||
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: totalUploadedBytes, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: totalUploadedMediaBytes)
|
||||
}
|
||||
|
||||
private func failWithError(_ error: ImportError) {
|
||||
self.stateValue = .error(error)
|
||||
for (_, disposable) in self.activeEntries {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private func complete() {
|
||||
guard let session = self.session else {
|
||||
self.failWithError(.generic)
|
||||
return
|
||||
}
|
||||
self.disposable.set((ChatHistoryImport.startImport(account: self.account, session: session)
|
||||
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.failWithError(.generic)
|
||||
}, completed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.stateValue = .done
|
||||
}))
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
guard let session = self.session else {
|
||||
return
|
||||
}
|
||||
if self.pendingEntries.isEmpty && self.activeEntries.isEmpty {
|
||||
self.complete()
|
||||
return
|
||||
}
|
||||
if case .error = self.stateValue {
|
||||
return
|
||||
}
|
||||
guard let archivePath = self.archivePath else {
|
||||
return
|
||||
}
|
||||
|
||||
while true {
|
||||
if self.activeEntries.count >= 3 {
|
||||
break
|
||||
}
|
||||
if self.pendingEntries.isEmpty {
|
||||
break
|
||||
}
|
||||
|
||||
let entry = self.pendingEntries.removeFirst()
|
||||
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
||||
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
|
||||
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
||||
let startTime = CACurrentMediaTime()
|
||||
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
||||
Logger.shared.log("ChatImportScreen", "[Done in \(CACurrentMediaTime() - startTime) s] Extract \(entry.0.path) to \(tempFile.path)")
|
||||
subscriber.putNext(tempFile)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let account = self.account
|
||||
|
||||
let uploadedEntrySignal: Signal<Float, ImportError> = unpackedFile
|
||||
|> mapToSignal { tempFile -> Signal<Float, ImportError> in
|
||||
let pathExtension = (entry.1 as NSString).pathExtension
|
||||
var mimeType = "application/octet-stream"
|
||||
if !pathExtension.isEmpty, let value = TGMimeTypeMap.mimeType(forExtension: pathExtension) {
|
||||
mimeType = value
|
||||
}
|
||||
return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, fileName: entry.0.path, mimeType: mimeType, type: entry.2)
|
||||
|> mapError { error -> ImportError in
|
||||
switch error {
|
||||
case .chatAdminRequired:
|
||||
return .chatAdminRequired
|
||||
case .generic:
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
self.activeEntries[entry.1] = disposable
|
||||
|
||||
disposable.set((uploadedEntrySignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let (size, _) = strongSelf.entryProgress[entry.0.path] {
|
||||
strongSelf.entryProgress[entry.0.path] = (size, Int(progress * Float(entry.0.uncompressedSize)))
|
||||
strongSelf.updateProgress()
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.failWithError(error)
|
||||
}, completed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activeEntries.removeValue(forKey: entry.0.path)
|
||||
strongSelf.updateState()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatImportActivityScreen: ViewController {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
private weak var controller: ChatImportActivityScreen?
|
||||
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private let doneAnimationNode: AnimatedStickerNode
|
||||
private let radialStatus: RadialStatusNode
|
||||
private let radialCheck: RadialStatusNode
|
||||
private let radialStatusBackground: ASImageNode
|
||||
private let radialStatusText: ImmediateTextNode
|
||||
private let progressText: ImmediateTextNode
|
||||
private let statusText: ImmediateTextNode
|
||||
|
||||
private let statusButtonText: ImmediateTextNode
|
||||
private let statusButton: HighlightableButtonNode
|
||||
private let doneButton: SolidRoundedButtonNode
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let totalBytes: Int
|
||||
private var state: ImportManager.State
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var feedback: HapticFeedback?
|
||||
|
||||
fileprivate var remainingAnimationSeconds: Double?
|
||||
|
||||
init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int, totalMediaBytes: Int) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.totalBytes = totalBytes
|
||||
self.state = .progress(totalBytes: totalBytes, totalUploadedBytes: 0, totalMediaBytes: totalMediaBytes, totalUploadedMediaBytes: 0)
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
self.doneAnimationNode = AnimatedStickerNode()
|
||||
self.doneAnimationNode.isHidden = true
|
||||
|
||||
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.radialCheck = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.radialStatusBackground = ASImageNode()
|
||||
self.radialStatusBackground.isUserInteractionEnabled = false
|
||||
self.radialStatusBackground.displaysAsynchronously = false
|
||||
self.radialStatusBackground.image = generateCircleImage(diameter: 180.0, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
|
||||
|
||||
self.radialStatusText = ImmediateTextNode()
|
||||
self.radialStatusText.isUserInteractionEnabled = false
|
||||
self.radialStatusText.displaysAsynchronously = false
|
||||
self.radialStatusText.maximumNumberOfLines = 1
|
||||
self.radialStatusText.isAccessibilityElement = false
|
||||
|
||||
self.progressText = ImmediateTextNode()
|
||||
self.progressText.isUserInteractionEnabled = false
|
||||
self.progressText.displaysAsynchronously = false
|
||||
self.progressText.maximumNumberOfLines = 1
|
||||
self.progressText.isAccessibilityElement = false
|
||||
|
||||
self.statusText = ImmediateTextNode()
|
||||
self.statusText.textAlignment = .center
|
||||
self.statusText.isUserInteractionEnabled = false
|
||||
self.statusText.displaysAsynchronously = false
|
||||
self.statusText.maximumNumberOfLines = 0
|
||||
self.statusText.isAccessibilityElement = false
|
||||
|
||||
self.statusButtonText = ImmediateTextNode()
|
||||
self.statusButtonText.isUserInteractionEnabled = false
|
||||
self.statusButtonText.displaysAsynchronously = false
|
||||
self.statusButtonText.maximumNumberOfLines = 1
|
||||
self.statusButtonText.isAccessibilityElement = false
|
||||
|
||||
self.statusButton = HighlightableButtonNode()
|
||||
|
||||
self.doneButton = SolidRoundedButtonNode(title: self.presentationData.strings.ChatImportActivity_OpenApp, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
if let path = getAppBundle().path(forResource: "HistoryImport", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 190 * 2, height: 190 * 2, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
if let path = getAppBundle().path(forResource: "HistoryImportDone", ofType: "tgs") {
|
||||
self.doneAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 190 * 2, height: 190 * 2, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.doneAnimationNode.started = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.animationNode.isHidden = true
|
||||
}
|
||||
self.doneAnimationNode.visibility = false
|
||||
}
|
||||
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.doneAnimationNode)
|
||||
self.addSubnode(self.radialStatusBackground)
|
||||
self.addSubnode(self.radialStatus)
|
||||
self.addSubnode(self.radialCheck)
|
||||
self.addSubnode(self.radialStatusText)
|
||||
self.addSubnode(self.progressText)
|
||||
self.addSubnode(self.statusText)
|
||||
self.addSubnode(self.statusButtonText)
|
||||
self.addSubnode(self.statusButton)
|
||||
self.addSubnode(self.doneButton)
|
||||
|
||||
self.statusButton.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.statusButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.statusButtonText.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.statusButtonText.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.statusButtonText.alpha = 1.0
|
||||
strongSelf.statusButtonText.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.animationNode.completed = { [weak self] stopped in
|
||||
guard let strongSelf = self, stopped else {
|
||||
return
|
||||
}
|
||||
strongSelf.animationNode.visibility = false
|
||||
strongSelf.doneAnimationNode.visibility = true
|
||||
strongSelf.doneAnimationNode.isHidden = false
|
||||
}
|
||||
|
||||
self.animationNode.frameUpdated = { [weak self] index, totalCount in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let remainingSeconds = Double(totalCount - index) / 60.0
|
||||
strongSelf.remainingAnimationSeconds = remainingSeconds
|
||||
strongSelf.controller?.updateProgressEstimation()
|
||||
}
|
||||
|
||||
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
|
||||
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
|
||||
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||
|
||||
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black)
|
||||
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
||||
videoNode.alpha = 0.01
|
||||
self.videoNode = videoNode
|
||||
|
||||
self.addSubnode(videoNode)
|
||||
videoNode.canAttachContent = true
|
||||
videoNode.play()
|
||||
|
||||
self.doneButton.pressed = { [weak self] in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
||||
let selector = NSSelectorFromString("openURL:")
|
||||
let url = URL(string: "tg://localpeer?id=\(controller.peerId.toInt64())")!
|
||||
application.perform(selector, with: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func statusButtonPressed() {
|
||||
switch self.state {
|
||||
case .done, .progress:
|
||||
self.controller?.cancel()
|
||||
case .error:
|
||||
self.controller?.beginImport()
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstLayout = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
let availableHeight = layout.size.height - navigationHeight
|
||||
|
||||
var iconSize = CGSize(width: 190.0, height: 190.0)
|
||||
var radialStatusSize = CGSize(width: 186.0, height: 186.0)
|
||||
var maxIconStatusSpacing: CGFloat = 46.0
|
||||
var maxProgressTextSpacing: CGFloat = 33.0
|
||||
var progressStatusSpacing: CGFloat = 14.0
|
||||
var statusButtonSpacing: CGFloat = 19.0
|
||||
|
||||
var maxK: CGFloat = availableHeight / (iconSize.height + maxIconStatusSpacing + 30.0 + maxProgressTextSpacing + 320.0)
|
||||
maxK = max(0.5, min(1.0, maxK))
|
||||
|
||||
iconSize.width = floor(iconSize.width * maxK)
|
||||
iconSize.height = floor(iconSize.height * maxK)
|
||||
radialStatusSize.width = floor(radialStatusSize.width * maxK)
|
||||
radialStatusSize.height = floor(radialStatusSize.height * maxK)
|
||||
maxIconStatusSpacing = floor(maxIconStatusSpacing * maxK)
|
||||
maxProgressTextSpacing = floor(maxProgressTextSpacing * maxK)
|
||||
progressStatusSpacing = floor(progressStatusSpacing * maxK)
|
||||
statusButtonSpacing = floor(statusButtonSpacing * maxK)
|
||||
|
||||
var updateRadialBackround = false
|
||||
if let width = self.radialStatusBackground.image?.size.width {
|
||||
if abs(width - radialStatusSize.width) > 0.01 {
|
||||
updateRadialBackround = true
|
||||
}
|
||||
} else {
|
||||
updateRadialBackround = true
|
||||
}
|
||||
|
||||
if updateRadialBackround {
|
||||
self.radialStatusBackground.image = generateCircleImage(diameter: radialStatusSize.width, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
|
||||
}
|
||||
|
||||
let effectiveProgress: CGFloat
|
||||
switch state {
|
||||
case let .progress(totalBytes, totalUploadedBytes, _, _):
|
||||
if totalBytes == 0 {
|
||||
effectiveProgress = 1.0
|
||||
} else {
|
||||
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
|
||||
}
|
||||
case .error:
|
||||
effectiveProgress = 0.0
|
||||
case .done:
|
||||
effectiveProgress = 1.0
|
||||
}
|
||||
|
||||
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: floor(36.0 * maxK), design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
switch self.state {
|
||||
case .progress, .done:
|
||||
self.statusButtonText.attributedText = NSAttributedString(string: self.presentationData.strings.Common_Done, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
||||
case .error:
|
||||
self.statusButtonText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_Retry, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
||||
}
|
||||
let statusButtonTextSize = self.statusButtonText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
switch self.state {
|
||||
case .progress:
|
||||
self.statusText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_InProgress, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
|
||||
case let .error(error):
|
||||
let errorText: String
|
||||
switch error {
|
||||
case .chatAdminRequired:
|
||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorNotAdmin
|
||||
case .invalidChatType:
|
||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorInvalidChatType
|
||||
case .generic:
|
||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorGeneric
|
||||
case .userBlocked:
|
||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorUserBlocked
|
||||
}
|
||||
self.statusText.attributedText = NSAttributedString(string: errorText, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemDestructiveColor)
|
||||
case .done:
|
||||
self.statusText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_Success, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
}
|
||||
|
||||
let statusTextSize = self.statusText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
let contentHeight: CGFloat
|
||||
var hideIcon = false
|
||||
if case .compact = layout.metrics.heightClass, layout.size.width > layout.size.height {
|
||||
hideIcon = true
|
||||
contentHeight = progressTextSize.height + progressStatusSpacing + 160.0
|
||||
} else {
|
||||
contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 140.0
|
||||
}
|
||||
|
||||
transition.updateAlpha(node: self.radialStatus, alpha: hideIcon ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.radialStatusBackground, alpha: hideIcon ? 0.0 : 1.0)
|
||||
switch self.state {
|
||||
case .done:
|
||||
break
|
||||
default:
|
||||
transition.updateAlpha(node: self.radialStatusText, alpha: hideIcon ? 0.0 : 1.0)
|
||||
}
|
||||
transition.updateAlpha(node: self.radialCheck, alpha: hideIcon ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.animationNode, alpha: hideIcon ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.doneAnimationNode, alpha: hideIcon ? 0.0 : 1.0)
|
||||
|
||||
let contentOriginY = navigationHeight + floor((layout.size.height - contentHeight) / 2.0)
|
||||
|
||||
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
|
||||
self.animationNode.updateLayout(size: iconSize)
|
||||
self.doneAnimationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
|
||||
self.doneAnimationNode.updateLayout(size: iconSize)
|
||||
|
||||
self.radialStatus.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - radialStatusSize.width) / 2.0), y: contentOriginY + iconSize.height + maxIconStatusSpacing), size: radialStatusSize)
|
||||
let checkSize: CGFloat = 130.0
|
||||
self.radialCheck.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - checkSize) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
self.radialStatusBackground.frame = self.radialStatus.frame
|
||||
|
||||
self.radialStatusText.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - radialStatusTextSize.width) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - radialStatusTextSize.height) / 2.0)), size: radialStatusTextSize)
|
||||
|
||||
self.progressText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressTextSize.width) / 2.0), y: hideIcon ? contentOriginY : (self.radialStatus.frame.maxY + maxProgressTextSpacing)), size: progressTextSize)
|
||||
|
||||
if case .progress = self.state {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.maxY + progressStatusSpacing), size: statusTextSize)
|
||||
self.statusButtonText.isHidden = true
|
||||
self.statusButton.isHidden = true
|
||||
self.doneButton.isHidden = true
|
||||
self.progressText.isHidden = false
|
||||
} else if case .error = self.state {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
|
||||
self.statusButtonText.isHidden = false
|
||||
self.statusButton.isHidden = false
|
||||
self.doneButton.isHidden = true
|
||||
self.progressText.isHidden = true
|
||||
} else {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
|
||||
self.statusButtonText.isHidden = false
|
||||
self.statusButton.isHidden = false
|
||||
self.doneButton.isHidden = true
|
||||
self.progressText.isHidden = true
|
||||
}/* else {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
|
||||
self.statusButtonText.isHidden = true
|
||||
self.statusButton.isHidden = true
|
||||
self.doneButton.isHidden = false
|
||||
self.progressText.isHidden = true
|
||||
}*/
|
||||
|
||||
let buttonSideInset: CGFloat = 75.0
|
||||
let buttonWidth = max(240.0, min(layout.size.width - buttonSideInset * 2.0, horizontalContainerFillingSizeForLayout(layout: layout, sideInset: buttonSideInset)))
|
||||
|
||||
let buttonHeight = self.doneButton.updateLayout(width: buttonWidth, transition: .immediate)
|
||||
|
||||
let doneButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing + 10.0), size: CGSize(width: buttonWidth, height: buttonHeight))
|
||||
self.doneButton.frame = doneButtonFrame
|
||||
|
||||
let statusButtonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusButtonTextSize.width) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing), size: statusButtonTextSize)
|
||||
self.statusButtonText.frame = statusButtonTextFrame
|
||||
self.statusButton.frame = statusButtonTextFrame.insetBy(dx: -10.0, dy: -10.0)
|
||||
|
||||
if isFirstLayout {
|
||||
self.updateState(state: self.state, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func transitionToDoneAnimation() {
|
||||
self.animationNode.stopAtNearestLoop = true
|
||||
}
|
||||
|
||||
func updateState(state: ImportManager.State, animated: Bool) {
|
||||
var wasDone = false
|
||||
if case .done = self.state {
|
||||
wasDone = true
|
||||
}
|
||||
self.state = state
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
|
||||
let effectiveProgress: CGFloat
|
||||
switch state {
|
||||
case let .progress(totalBytes, totalUploadedBytes, _, _):
|
||||
if totalBytes == 0 {
|
||||
effectiveProgress = 1.0
|
||||
} else {
|
||||
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
|
||||
}
|
||||
case .error:
|
||||
effectiveProgress = 0.0
|
||||
case .done:
|
||||
effectiveProgress = 1.0
|
||||
}
|
||||
self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.01, effectiveProgress), cancelEnabled: false, animateRotation: false), animated: animated, synchronous: true, completion: {})
|
||||
if case .done = state {
|
||||
self.radialCheck.transitionToState(.progress(color: .clear, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: false, synchronous: true, completion: {})
|
||||
self.radialCheck.transitionToState(.check(self.presentationData.theme.list.itemAccentColor), animated: animated, synchronous: true, completion: {})
|
||||
self.radialStatus.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.radialStatus.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
|
||||
})
|
||||
self.radialStatusBackground.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.radialStatusBackground.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
|
||||
})
|
||||
self.radialCheck.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.radialCheck.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
|
||||
})
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
transition.updateAlpha(node: self.radialStatusText, alpha: 0.0)
|
||||
|
||||
if !wasDone {
|
||||
self.view.addSubview(ConfettiView(frame: self.view.bounds))
|
||||
|
||||
if self.feedback == nil {
|
||||
self.feedback = HapticFeedback()
|
||||
}
|
||||
self.feedback?.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let cancel: () -> Void
|
||||
fileprivate var peerId: PeerId
|
||||
private let archivePath: String?
|
||||
private let mainEntry: TempBoxFile
|
||||
private let totalBytes: Int
|
||||
private let totalMediaBytes: Int
|
||||
private let otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||
|
||||
private var importManager: ImportManager?
|
||||
private var progressEstimator: ProgressEstimator?
|
||||
private var totalMediaProgress: Float = 0.0
|
||||
private var beganCompletion: Bool = false
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private let progressDisposable = MetaDisposable()
|
||||
|
||||
override public var _presentedInModal: Bool {
|
||||
get {
|
||||
return true
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
|
||||
self.context = context
|
||||
self.cancel = cancel
|
||||
self.peerId = peerId
|
||||
self.archivePath = archivePath
|
||||
self.mainEntry = mainEntry
|
||||
|
||||
self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, ChatHistoryImport.MediaType) in
|
||||
return (entry.0, entry.1, entry.2)
|
||||
}
|
||||
|
||||
let mainEntrySize = fileSize(self.mainEntry.path) ?? 0
|
||||
|
||||
var totalMediaBytes = 0
|
||||
for entry in self.otherEntries {
|
||||
totalMediaBytes += Int(entry.0.uncompressedSize)
|
||||
}
|
||||
self.totalBytes = mainEntrySize + totalMediaBytes
|
||||
self.totalMediaBytes = totalMediaBytes
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: true))
|
||||
|
||||
self.title = self.presentationData.strings.ChatImportActivity_Title
|
||||
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
|
||||
|
||||
self.attemptNavigation = { _ in
|
||||
return false
|
||||
}
|
||||
|
||||
self.beginImport()
|
||||
|
||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
||||
application.isIdleTimerDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.progressDisposable.dispose()
|
||||
|
||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
||||
application.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.cancel()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self, context: self.context, totalBytes: self.totalBytes, totalMediaBytes: self.totalMediaBytes)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
private func beginImport() {
|
||||
self.progressEstimator = ProgressEstimator()
|
||||
self.beganCompletion = false
|
||||
|
||||
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
|
||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId)
|
||||
|> mapError { _ -> ImportManager.ImportError in
|
||||
return .generic
|
||||
}
|
||||
} else {
|
||||
resolvedPeerId = .single(self.peerId)
|
||||
}
|
||||
|
||||
self.disposable.set((resolvedPeerId
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerId in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let importManager = ImportManager(account: strongSelf.context.account, peerId: peerId, mainFile: strongSelf.mainEntry, archivePath: strongSelf.archivePath, entries: strongSelf.otherEntries)
|
||||
strongSelf.importManager = importManager
|
||||
strongSelf.progressDisposable.set((importManager.state
|
||||
|> deliverOnMainQueue).start(next: { state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateState(state: state, animated: true)
|
||||
if case let .progress(_, _, totalMediaBytes, totalUploadedMediaBytes) = state {
|
||||
let progress = Float(totalUploadedMediaBytes) / Float(totalMediaBytes)
|
||||
strongSelf.totalMediaProgress = progress
|
||||
}
|
||||
}))
|
||||
}, error: { [weak self] error in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateState(state: .error(error), animated: true)
|
||||
}))
|
||||
}
|
||||
|
||||
fileprivate func updateProgressEstimation() {
|
||||
if !self.beganCompletion, let progressEstimator = self.progressEstimator, let remainingAnimationSeconds = self.controllerNode.remainingAnimationSeconds {
|
||||
if let remainingSeconds = progressEstimator.update(progress: self.totalMediaProgress) {
|
||||
//print("remainingSeconds: \(remainingSeconds)")
|
||||
//print("remainingAnimationSeconds + 1.0: \(remainingAnimationSeconds + 1.0)")
|
||||
if remainingSeconds <= remainingAnimationSeconds + 1.0 {
|
||||
self.beganCompletion = true
|
||||
self.controllerNode.transitionToDoneAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ public enum ChatListSearchItemHeaderType {
|
||||
case groupMembers
|
||||
case activeVoiceChats
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
|
||||
fileprivate func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
@ -68,6 +69,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
return strings.CallList_ActiveVoiceChatsHeader
|
||||
case .recentCalls:
|
||||
return strings.CallList_RecentCallsHeader
|
||||
case .orImportIntoAnExistingGroup:
|
||||
return strings.ChatList_HeaderImportIntoAnExistingGroup
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +116,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
return .activeVoiceChats
|
||||
case .recentCalls:
|
||||
return .recentCalls
|
||||
case .orImportIntoAnExistingGroup:
|
||||
return .orImportIntoAnExistingGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,6 +147,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
|
||||
case groupMembers
|
||||
case activeVoiceChats
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
@ -58,6 +58,7 @@ swift_library(
|
||||
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -16,6 +16,7 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
let context: AccountContext
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
let appearance: ChatListNodeAdditionalCategory.Appearance
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
@ -29,7 +30,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
context: AccountContext,
|
||||
title: String,
|
||||
image: UIImage?,
|
||||
appearance: ChatListNodeAdditionalCategory.Appearance,
|
||||
isSelected: Bool,
|
||||
header: ListViewItemHeader?,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
@ -37,10 +40,16 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.appearance = appearance
|
||||
self.isSelected = isSelected
|
||||
self.action = action
|
||||
|
||||
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
switch appearance {
|
||||
case .option:
|
||||
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
case .action:
|
||||
self.header = header
|
||||
}
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
@ -81,6 +90,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
}
|
||||
|
||||
public func selected(listView: ListView) {
|
||||
if case .action = self.appearance {
|
||||
listView.clearHighlightAnimated(true)
|
||||
}
|
||||
self.action()
|
||||
}
|
||||
|
||||
@ -107,6 +119,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
} else if let _ = nextItem as? ChatListAdditionalCategoryItem {
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
} else {
|
||||
last = true
|
||||
@ -172,16 +187,37 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
return
|
||||
if let item = self.item, case .action = item.appearance {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
/*super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
self.isHighlighted = highlighted
|
||||
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)*/
|
||||
self.isHighlighted = highlighted
|
||||
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
let reallyHighlighted = self.isHighlighted
|
||||
let highlightProgress: CGFloat = 1.0
|
||||
|
||||
if reallyHighlighted {
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
self.highlightedBackgroundNode.layer.removeAllAnimations()
|
||||
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 1.0 - highlightProgress, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ChatListAdditionalCategoryItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
|
||||
@ -206,20 +242,29 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
let updatedSelectionNode: CheckNode?
|
||||
let isSelected = item.isSelected
|
||||
|
||||
rightInset += 28.0
|
||||
if case .option = item.appearance {
|
||||
rightInset += 28.0
|
||||
|
||||
let selectionNode: CheckNode
|
||||
if let current = currentSelectionNode {
|
||||
selectionNode = current
|
||||
updatedSelectionNode = selectionNode
|
||||
let selectionNode: CheckNode
|
||||
if let current = currentSelectionNode {
|
||||
selectionNode = current
|
||||
updatedSelectionNode = selectionNode
|
||||
} else {
|
||||
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
|
||||
selectionNode.isUserInteractionEnabled = false
|
||||
updatedSelectionNode = selectionNode
|
||||
}
|
||||
} else {
|
||||
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
|
||||
selectionNode.isUserInteractionEnabled = false
|
||||
updatedSelectionNode = selectionNode
|
||||
updatedSelectionNode = nil
|
||||
}
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
let textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
let textColor: UIColor
|
||||
if case .action = item.appearance {
|
||||
textColor = item.presentationData.theme.list.itemAccentColor
|
||||
} else {
|
||||
textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
}
|
||||
titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: textColor)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
@ -261,11 +306,12 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.image = item.image
|
||||
|
||||
strongSelf.topSeparatorNode.isHidden = true
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
|
||||
if let image = item.image {
|
||||
strongSelf.avatarNode.image = item.image
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0 + floor((avatarDiameter - image.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - image.size.width) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0))
|
||||
|
@ -24,6 +24,7 @@ import LocalizedPeerData
|
||||
import TelegramIntents
|
||||
import TooltipUI
|
||||
import TelegramCallsUI
|
||||
import StickerResources
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
@ -143,6 +144,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private let featuredFiltersDisposable = MetaDisposable()
|
||||
private var processedFeaturedFilters = false
|
||||
|
||||
private let preloadedSticker = Promise<TelegramMediaFile?>(nil)
|
||||
private let preloadStickerDisposable = MetaDisposable()
|
||||
|
||||
private let isReorderingTabsValue = ValuePromise<Bool>(false)
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
@ -583,7 +587,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
strongSelf.deletePeerChat(peerId: peerId, joined: joined)
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in
|
||||
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, activateInput, promoInfo in
|
||||
if let strongSelf = self {
|
||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||||
var scrollToEndIfExists = false
|
||||
@ -591,43 +595,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
scrollToEndIfExists = true
|
||||
}
|
||||
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
if let promoInfo = promoInfo {
|
||||
switch promoInfo {
|
||||
case .proxy:
|
||||
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
|
||||
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||||
}
|
||||
})
|
||||
case let .psa(type, _):
|
||||
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
|
||||
let key = "ChatList.PsaAlert.\(type)"
|
||||
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
|
||||
text = string
|
||||
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
|
||||
text = string
|
||||
}
|
||||
let _ = (strongSelf.preloadedSticker.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] greetingSticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
if let promoInfo = promoInfo {
|
||||
switch promoInfo {
|
||||
case .proxy:
|
||||
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
|
||||
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||||
}
|
||||
})
|
||||
case let .psa(type, _):
|
||||
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|
||||
|> deliverOnMainQueue).start(next: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
|
||||
let key = "ChatList.PsaAlert.\(type)"
|
||||
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
|
||||
text = string
|
||||
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
|
||||
text = string
|
||||
}
|
||||
|
||||
controller.displayPromoAnnouncement(text: text)
|
||||
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
|
||||
controller.displayPromoAnnouncement(text: text)
|
||||
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
if activateInput {
|
||||
strongSelf.prepareRandomGreetingSticker()
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1077,6 +1091,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
public override func displayNodeDidLoad() {
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
self.prepareRandomGreetingSticker()
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
@ -1212,8 +1234,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
strongSelf.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
switch controller.content {
|
||||
case let .archivedChat(archivedChat):
|
||||
if peerIds.contains(PeerId(archivedChat.peerId)) {
|
||||
case let .archivedChat(peerId, _, _, _):
|
||||
if peerIds.contains(PeerId(peerId)) {
|
||||
controller.dismiss()
|
||||
}
|
||||
default:
|
||||
@ -1970,6 +1992,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
let _ = (signal
|
||||
|> deliverOnMainQueue).start()
|
||||
|
||||
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
|
||||
var state = state
|
||||
for peerId in peerIds {
|
||||
state.selectedPeerIds.remove(peerId)
|
||||
}
|
||||
return state
|
||||
})
|
||||
|
||||
return true
|
||||
} else if value == .undo {
|
||||
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
|
||||
@ -2117,6 +2148,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally {
|
||||
@ -2126,43 +2159,89 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
var items: [ActionSheetItem] = []
|
||||
var canClear = true
|
||||
var canStop = false
|
||||
var canRemoveGlobally = false
|
||||
|
||||
var deleteTitle = strongSelf.presentationData.strings.Common_Delete
|
||||
if let channel = chatPeer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
canClear = false
|
||||
deleteTitle = strongSelf.presentationData.strings.Channel_LeaveChannel
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else {
|
||||
deleteTitle = strongSelf.presentationData.strings.Group_LeaveGroup
|
||||
deleteTitle = strongSelf.presentationData.strings.Group_DeleteGroup
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
}
|
||||
if let addressName = channel.addressName, !addressName.isEmpty {
|
||||
canClear = false
|
||||
}
|
||||
} else if let group = chatPeer as? TelegramGroup {
|
||||
if case .creator = group.role {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if let user = chatPeer as? TelegramUser, user.botInfo != nil {
|
||||
canStop = !user.flags.contains(.isSupport)
|
||||
canClear = user.botInfo == nil
|
||||
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
||||
} else if let _ = chatPeer as? TelegramSecretChat {
|
||||
canClear = true
|
||||
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
||||
}
|
||||
|
||||
var canRemoveGlobally = false
|
||||
let limitsConfiguration = strongSelf.context.currentLimitsConfiguration.with { $0 }
|
||||
if chatPeer is TelegramUser && chatPeer.id != strongSelf.context.account.peerId {
|
||||
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if chatPeer is TelegramSecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
if canClear {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if canRemoveGlobally, (mainPeer is TelegramGroup || mainPeer is TelegramChannel) {
|
||||
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 let channel = mainPeer as? TelegramChannel, 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 let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
|
||||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteChannelConfirmation
|
||||
} else {
|
||||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteGroupConfirmation
|
||||
}
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||
})
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
} else {
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
|
||||
if canClear {
|
||||
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -2207,57 +2286,133 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}), in: .current)
|
||||
}
|
||||
|
||||
if canRemoveGlobally {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, 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)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forEveryone)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forLocalPeer)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
|
||||
beginClear(.forLocalPeer)
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
if chatPeer is TelegramSecretChat {
|
||||
beginClear(.forEveryone)
|
||||
} else {
|
||||
if canRemoveGlobally {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, 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)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forLocalPeer)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forEveryone)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
|
||||
beginClear(.forLocalPeer)
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
|
||||
}))
|
||||
if chatPeer is TelegramSecretChat {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if canRemoveGlobally, (mainPeer is TelegramGroup || mainPeer is TelegramChannel) {
|
||||
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 let channel = mainPeer as? TelegramChannel, 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 let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
|
||||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribersConfirmationText
|
||||
} else {
|
||||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText
|
||||
}
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||
})
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if canStop {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
|
||||
@ -2302,6 +2457,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let user = chatPeer as? TelegramUser, user.botInfo != nil {
|
||||
canRemoveGlobally = false
|
||||
}
|
||||
if let _ = chatPeer as? TelegramSecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
if canRemoveGlobally {
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
@ -2318,6 +2476,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
completion(true)
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
@ -2335,13 +2500,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
@ -2524,6 +2682,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
deleteSendMessageIntents(peerId: peerId)
|
||||
})
|
||||
|
||||
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
|
||||
var state = state
|
||||
state.selectedPeerIds.remove(peerId)
|
||||
return state
|
||||
})
|
||||
|
||||
completion()
|
||||
return true
|
||||
} else if value == .undo {
|
||||
@ -2566,6 +2731,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareRandomGreetingSticker() {
|
||||
let context = self.context
|
||||
self.preloadedSticker.set(.single(nil)
|
||||
|> then(randomGreetingSticker(account: context.account)
|
||||
|> map { item in
|
||||
return item?.file
|
||||
}))
|
||||
|
||||
self.preloadStickerDisposable.set((self.preloadedSticker.get()
|
||||
|> mapToSignal { sticker -> Signal<Void, NoError> in
|
||||
if let sticker = sticker {
|
||||
let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start()
|
||||
return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
}
|
||||
|
||||
override public func tabBarDisabledAction() {
|
||||
self.donePressed()
|
||||
}
|
||||
|
@ -475,8 +475,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in
|
||||
self?.deletePeerChat?(peerId, joined)
|
||||
}
|
||||
itemNode.listNode.peerSelected = { [weak self] peerId, a, b in
|
||||
self?.peerSelected?(peerId, a, b)
|
||||
itemNode.listNode.peerSelected = { [weak self] peerId, animated, activateInput, promoInfo in
|
||||
self?.peerSelected?(peerId, animated, activateInput, promoInfo)
|
||||
}
|
||||
itemNode.listNode.groupSelected = { [weak self] groupId in
|
||||
self?.groupSelected?(groupId)
|
||||
@ -522,7 +522,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var toggleArchivedFolderHiddenByDefault: (() -> Void)?
|
||||
var hidePsa: ((PeerId) -> Void)?
|
||||
var deletePeerChat: ((PeerId, Bool) -> Void)?
|
||||
var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
|
||||
var peerSelected: ((Peer, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
|
||||
var groupSelected: ((PeerGroupId) -> Void)?
|
||||
var updatePeerGrouping: ((PeerId, Bool) -> Void)?
|
||||
var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
|
||||
|
@ -685,7 +685,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
|> map { actions -> [ContextMenuItem] in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
|
||||
if let linkForCopying = linkForCopying {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
c.dismiss(completion: {})
|
||||
@ -896,7 +895,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}).start()
|
||||
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in
|
||||
let peerId = peer.id
|
||||
if let strongSelf = self, let _ = peerSelectionController {
|
||||
if peerId == strongSelf.context.account.peerId {
|
||||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in
|
||||
@ -948,17 +948,25 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
})
|
||||
}) |> deliverOnMainQueue).start(completed: {
|
||||
if let strongSelf = self {
|
||||
// strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
|
||||
|
||||
let controller = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))
|
||||
controller.purposefulAction = { [weak self] in
|
||||
self?.cancel?()
|
||||
}
|
||||
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
|
||||
if let peerSelectionController = peerSelectionController {
|
||||
peerSelectionController.dismiss()
|
||||
|
||||
if let navigationController = strongSelf.navigationController, let peerSelectionControllerIndex = navigationController.viewControllers.firstIndex(where: { $0 is PeerSelectionController }) {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
viewControllers.insert(controller, at: peerSelectionControllerIndex)
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
Queue.mainQueue().after(0.2) {
|
||||
peerSelectionController?.dismiss()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
|
||||
if let peerSelectionController = peerSelectionController {
|
||||
peerSelectionController.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
strongSelf.updateState { state in
|
||||
return state.withUpdatedSelectedMessageIds(nil)
|
||||
|
@ -26,6 +26,7 @@ import GalleryData
|
||||
import AppBundle
|
||||
import ShimmerEffect
|
||||
import ChatListSearchRecentPeersNode
|
||||
import UndoUI
|
||||
|
||||
private enum ChatListRecentEntryStableId: Hashable {
|
||||
case topPeers
|
||||
@ -429,7 +430,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .collapse:
|
||||
actionTitle = strings.ChatList_Search_ShowLess
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if filter.contains(.onlyGroups) {
|
||||
headerType = .chats
|
||||
} else {
|
||||
headerType = .localPeers
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
|
||||
toggleExpandLocalResults()
|
||||
})
|
||||
}
|
||||
@ -1744,6 +1751,35 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
|
||||
|
||||
if let controller = strongSelf.navigationController?.topViewController as? ViewController {
|
||||
var hasTooltip = false
|
||||
controller.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
hasTooltip = true
|
||||
controller.dismissWithCommitAction()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let slowdown = baseRate == .x1
|
||||
controller.present(
|
||||
UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .audioRate(
|
||||
slowdown: slowdown,
|
||||
text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp
|
||||
),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: hasTooltip,
|
||||
action: { action in
|
||||
return true
|
||||
}
|
||||
),
|
||||
in: .current
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
mediaAccessoryPanel.togglePlayPause = { [weak self] in
|
||||
|
@ -234,7 +234,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
switch status {
|
||||
case let .Fetching(_, progress):
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Local:
|
||||
statusState = .none
|
||||
case .Remote:
|
||||
|
@ -130,8 +130,6 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
|
||||
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
|
||||
var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var requestExpandTabs: (() -> Bool)?
|
||||
|
||||
|
@ -479,11 +479,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
switch item.content {
|
||||
case .groupReference:
|
||||
return nil
|
||||
case let .peer(peer):
|
||||
guard let chatMainPeer = peer.peer.chatMainPeer else {
|
||||
case let .peer(_, peer, combinedReadState, _, _, _, _, _, _, _, _, _):
|
||||
guard let chatMainPeer = peer.chatMainPeer else {
|
||||
return nil
|
||||
}
|
||||
return chatMainPeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
var result = ""
|
||||
if item.context.account.peerId == chatMainPeer.id {
|
||||
result += item.presentationData.strings.DialogList_SavedMessages
|
||||
} else {
|
||||
result += chatMainPeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
}
|
||||
if let combinedReadState = combinedReadState, combinedReadState.count > 0 {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_Chat_UnreadMessages(combinedReadState.count))"
|
||||
}
|
||||
return result
|
||||
}
|
||||
} set(value) {
|
||||
}
|
||||
@ -497,25 +506,25 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
switch item.content {
|
||||
case .groupReference:
|
||||
return nil
|
||||
case let .peer(peer):
|
||||
if let message = peer.messages.last {
|
||||
case let .peer(messages, peer, combinedReadState, _, _, _, _, _, _, _, _, _):
|
||||
if let message = messages.last {
|
||||
var result = ""
|
||||
if message.flags.contains(.Incoming) {
|
||||
result += "Message"
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_Message
|
||||
} else {
|
||||
result += "Outgoing message"
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||
}
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: peer.messages, chatPeer: peer.peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
||||
result += "\nFrom: \(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))"
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
||||
}
|
||||
if !message.flags.contains(.Incoming), let combinedReadState = peer.combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
|
||||
result += "\nRead"
|
||||
if !message.flags.contains(.Incoming), let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageRead)"
|
||||
}
|
||||
result += "\n\(messageText)"
|
||||
return result
|
||||
} else {
|
||||
return "Empty"
|
||||
return item.presentationData.strings.VoiceOver_ChatList_MessageEmpty
|
||||
}
|
||||
}
|
||||
} set(value) {
|
||||
@ -958,7 +967,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if let message = messages.last, let author = message.author as? TelegramUser, let peer = itemPeer.chatMainPeer, !(peer is TelegramUser) {
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
} else if !displayAsMessage {
|
||||
peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
|
||||
peerText = authorSignature
|
||||
} else {
|
||||
peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1258,6 +1271,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
if peer.isScam {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isFake {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isVerified {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
|
||||
credibilityIconOffset = 3.0
|
||||
@ -1270,6 +1286,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
if peer.isScam {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isFake {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isVerified {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
|
||||
credibilityIconOffset = 3.0
|
||||
@ -1354,7 +1373,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
switch item.content {
|
||||
case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _):
|
||||
if !displayAsMessage {
|
||||
if let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
|
||||
if let peer = renderedPeer.chatMainPeer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
|
||||
let updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0)
|
||||
let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp)
|
||||
if case .online = relativeStatus {
|
||||
|
@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
}
|
||||
default:
|
||||
hideAuthor = true
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) {
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
}
|
||||
}
|
||||
case _ as TelegramMediaExpiredContent:
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) {
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
}
|
||||
case let poll as TelegramMediaPoll:
|
||||
|
@ -165,13 +165,20 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch entry.entry {
|
||||
case .HeaderEntry:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
|
||||
case let .AdditionalCategory(_, id, title, image, selected, presentationData):
|
||||
case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData):
|
||||
var header: ChatListSearchItemHeader?
|
||||
if case .action = appearance {
|
||||
// TODO: hack, generalize
|
||||
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
context: context,
|
||||
title: title,
|
||||
image: image,
|
||||
appearance: appearance,
|
||||
isSelected: selected,
|
||||
header: header,
|
||||
action: {
|
||||
nodeInteraction.additionalCategorySelected(id)
|
||||
}
|
||||
@ -249,7 +256,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if case .action = additionalCategories[0].appearance {
|
||||
// TODO: hack, generalize
|
||||
headerType = .orImportIntoAnExistingGroup
|
||||
} else {
|
||||
headerType = .chats
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -319,7 +333,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if case .action = additionalCategories[0].appearance {
|
||||
// TODO: hack, generalize
|
||||
headerType = .orImportIntoAnExistingGroup
|
||||
} else {
|
||||
headerType = .chats
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -355,13 +376,20 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case .HeaderEntry:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
|
||||
case let .AdditionalCategory(index: _, id, title, image, selected, presentationData):
|
||||
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
|
||||
var header: ChatListSearchItemHeader?
|
||||
if case .action = appearance {
|
||||
// TODO: hack, generalize
|
||||
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
context: context,
|
||||
title: title,
|
||||
image: image,
|
||||
appearance: appearance,
|
||||
isSelected: selected,
|
||||
header: header,
|
||||
action: {
|
||||
nodeInteraction.additionalCategorySelected(id)
|
||||
}
|
||||
@ -424,7 +452,7 @@ public final class ChatListNode: ListView {
|
||||
return _contentsReady.get()
|
||||
}
|
||||
|
||||
public var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
|
||||
public var peerSelected: ((Peer, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
|
||||
public var disabledPeerSelected: ((Peer) -> Void)?
|
||||
public var additionalCategorySelected: ((Int) -> Void)?
|
||||
public var groupSelected: ((PeerGroupId) -> Void)?
|
||||
@ -549,7 +577,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
}, peerSelected: { [weak self] peer, promoInfo in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peer, true, promoInfo)
|
||||
peerSelected(peer, true, true, promoInfo)
|
||||
}
|
||||
}, disabledPeerSelected: { [weak self] peer in
|
||||
if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected {
|
||||
@ -578,7 +606,18 @@ public final class ChatListNode: ListView {
|
||||
self?.additionalCategorySelected?(id)
|
||||
}, messageSelected: { [weak self] peer, message, promoInfo in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peer, true, promoInfo)
|
||||
var activateInput = false
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case .peerJoined, .groupCreated, .channelMigratedFromGroup, .historyCleared:
|
||||
activateInput = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
peerSelected(peer, true, activateInput, promoInfo)
|
||||
}
|
||||
}, groupSelected: { [weak self] groupId in
|
||||
if let strongSelf = self, let groupSelected = strongSelf.groupSelected {
|
||||
@ -1734,7 +1773,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter)
|
||||
strongSelf.setChatListLocation(location)
|
||||
strongSelf.peerSelected?(peer, false, nil)
|
||||
strongSelf.peerSelected?(peer, false, false, nil)
|
||||
})
|
||||
case .previous(unread: false), .next(unread: false):
|
||||
var target: (ChatListIndex, Peer)? = nil
|
||||
@ -1758,7 +1797,7 @@ public final class ChatListNode: ListView {
|
||||
if let target = target {
|
||||
let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: self.chatListFilter)
|
||||
self.setChatListLocation(location)
|
||||
self.peerSelected?(target.1, false, nil)
|
||||
self.peerSelected?(target.1, false, false, nil)
|
||||
}
|
||||
case let .peerId(peerId):
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
|
||||
@ -1768,7 +1807,7 @@ public final class ChatListNode: ListView {
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
strongSelf.peerSelected?(peer, false, nil)
|
||||
strongSelf.peerSelected?(peer, false, false, nil)
|
||||
})
|
||||
case let .index(index):
|
||||
guard index < 10 else {
|
||||
@ -1787,7 +1826,7 @@ public final class ChatListNode: ListView {
|
||||
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[10 - index - 1] {
|
||||
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
|
||||
self.setChatListLocation(location)
|
||||
self.peerSelected?(renderedPeer.peer!, false, nil)
|
||||
self.peerSelected?(renderedPeer.peer!, false, false, nil)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -5,6 +5,7 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
|
||||
enum ChatListNodeEntryId: Hashable {
|
||||
case Header
|
||||
@ -50,7 +51,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
case HoleEntry(ChatListHole, theme: PresentationTheme)
|
||||
case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool)
|
||||
case ArchiveIntro(presentationData: ChatListPresentationData)
|
||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, selected: Bool, presentationData: ChatListPresentationData)
|
||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
|
||||
|
||||
var sortIndex: ChatListNodeEntrySortIndex {
|
||||
switch self {
|
||||
@ -242,8 +243,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsSelected, lhsPresentationData):
|
||||
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsSelected, rhsPresentationData) = rhs {
|
||||
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsAppearance, lhsSelected, lhsPresentationData):
|
||||
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsAppearance, rhsSelected, rhsPresentationData) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
@ -256,6 +257,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
if lhsImage !== rhsImage {
|
||||
return false
|
||||
}
|
||||
if lhsAppearance != rhsAppearance {
|
||||
return false
|
||||
}
|
||||
if lhsSelected != rhsSelected {
|
||||
return false
|
||||
}
|
||||
@ -374,7 +378,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
|
||||
_) = mode {
|
||||
var index = 0
|
||||
for category in additionalCategories.reversed(){
|
||||
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -459,14 +459,14 @@ private class ChatListStatusProgressNode: ChatListStatusContentNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false))
|
||||
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true))
|
||||
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
||||
if case let .progress(color, progress) = state {
|
||||
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false), animated: animated, completion: {})
|
||||
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,12 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
|
||||
let string: NSAttributedString
|
||||
if activities.count > 1 {
|
||||
let peerTitle = activities[0].0.compactDisplayTitle
|
||||
string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).0).0, font: textFont, textColor: color)
|
||||
if activities.count == 2 {
|
||||
let secondPeerTitle = activities[1].0.compactDisplayTitle
|
||||
string = NSAttributedString(string: strings.DialogList_MultipleTypingPair(peerTitle, secondPeerTitle).0, font: textFont, textColor: color)
|
||||
} else {
|
||||
string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).0).0, font: textFont, textColor: color)
|
||||
}
|
||||
} else {
|
||||
string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color)
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
isCompact = true
|
||||
originY = -1.0 - UIScreenPixel
|
||||
case .compactFetching:
|
||||
state = .progress(color: .white, lineWidth: nil, value: 0.0, cancelEnabled: true)
|
||||
state = .progress(color: .white, lineWidth: nil, value: 0.0, cancelEnabled: true, animateRotation: true)
|
||||
isCompact = true
|
||||
originY = -1.0
|
||||
}
|
||||
|
16
submodules/ConfettiEffect/BUILD
Normal file
16
submodules/ConfettiEffect/BUILD
Normal file
@ -0,0 +1,16 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ConfettiEffect",
|
||||
module_name = "ConfettiEffect",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -44,13 +44,13 @@ private final class ParticleLayer: CALayer {
|
||||
}
|
||||
}
|
||||
|
||||
final class ConfettiView: UIView {
|
||||
public final class ConfettiView: UIView {
|
||||
private var particles: [ParticleLayer] = []
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
|
||||
private var localTime: Float = 0.0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
@ -142,7 +142,7 @@ final class ConfettiView: UIView {
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
|
||||
interaction.activateSearch()
|
||||
})
|
||||
case let .sort(theme, strings, sortOrder):
|
||||
case let .sort(_, strings, sortOrder):
|
||||
var text = strings.Contacts_SortedByName
|
||||
if case .presence = sortOrder {
|
||||
text = strings.Contacts_SortedByPresence
|
||||
@ -166,17 +166,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
|
||||
interaction.openSortMenu()
|
||||
})
|
||||
case let .permissionInfo(theme, title, text, suppressed):
|
||||
case let .permissionInfo(_, title, text, suppressed):
|
||||
return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
|
||||
interaction.suppressWarning()
|
||||
})
|
||||
case let .permissionEnable(theme, text):
|
||||
case let .permissionEnable(_, text):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
|
||||
interaction.authorize()
|
||||
})
|
||||
case let .option(_, option, header, theme, _):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action)
|
||||
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
|
||||
case let .option(_, option, header, _, _):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action)
|
||||
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
|
||||
var status: ContactsPeerItemStatus
|
||||
let itemPeer: ContactsPeerItemPeer
|
||||
var isContextActionEnabled = false
|
||||
@ -928,9 +928,9 @@ public final class ContactListNode: ASDisplayNode {
|
||||
|> mapToSignal { presentation in
|
||||
var generateSections = false
|
||||
var includeChatList = false
|
||||
if case let .natural(natural) = presentation {
|
||||
if case let .natural(_, includeChatListValue) = presentation {
|
||||
generateSections = true
|
||||
includeChatList = natural.includeChatList
|
||||
includeChatList = includeChatListValue
|
||||
}
|
||||
|
||||
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {
|
||||
|
@ -438,6 +438,10 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
var hasAdditionalActions: Bool {
|
||||
return self.additionalActionsNode != nil
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) {
|
||||
self.blurBackground = blurBackground
|
||||
self.shadowNode = ASImageNode()
|
||||
@ -534,4 +538,14 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
func animateIn() {
|
||||
self.textSelectionTipNode?.animateIn()
|
||||
}
|
||||
|
||||
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let additionalActionsNode = self.additionalActionsNode else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
@ -1377,15 +1377,28 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let previousActionsContainerNode = previousActionsContainerNode {
|
||||
if transition.isAnimated {
|
||||
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1)
|
||||
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
|
||||
previousActionsContainerNode?.removeFromSupernode()
|
||||
})
|
||||
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions {
|
||||
var initialFrame = self.actionsContainerNode.frame
|
||||
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
|
||||
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
|
||||
transition.animateFrame(node: self.actionsContainerNode, from: initialFrame)
|
||||
transition.animatePosition(node: previousActionsContainerNode, to: CGPoint(x: 0.0, y: -delta), removeOnCompletion: false, additive: true)
|
||||
previousActionsContainerNode.animateOut(offset: delta, transition: transition)
|
||||
|
||||
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
|
||||
if transition.isAnimated {
|
||||
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
|
||||
previousActionsContainerNode?.removeFromSupernode()
|
||||
})
|
||||
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1)
|
||||
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
|
||||
previousActionsContainerNode?.removeFromSupernode()
|
||||
})
|
||||
|
||||
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
|
||||
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
} else {
|
||||
|
@ -58,15 +58,21 @@ private func loadCountryCodes() -> [(String, Int)] {
|
||||
|
||||
private let countryCodes: [(String, Int)] = loadCountryCodes()
|
||||
|
||||
func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] {
|
||||
func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] {
|
||||
let locale = localeWithStrings(strings)
|
||||
var result: [((String, String), String, Int)] = []
|
||||
var result: [((String, String), String, [Int])] = []
|
||||
for country in AuthorizationSequenceCountrySelectionController.countries() {
|
||||
if country.hidden {
|
||||
continue
|
||||
}
|
||||
if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id), let codeValue = country.countryCodes.first?.code, let code = Int(codeValue) {
|
||||
result.append(((englishCountryName, countryName), country.id, code))
|
||||
if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id) {
|
||||
var codes: [Int] = []
|
||||
for codeValue in country.countryCodes {
|
||||
if let code = Int(codeValue.code) {
|
||||
codes.append(code)
|
||||
}
|
||||
}
|
||||
result.append(((englishCountryName, countryName), country.id, codes))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
@ -128,7 +134,7 @@ private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey
|
||||
return false
|
||||
}
|
||||
|
||||
private func searchCountries(items: [((String, String), String, Int)], query: String) -> [((String, String), String, Int)] {
|
||||
private func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] {
|
||||
let queryTokens = stringTokens(query.lowercased())
|
||||
|
||||
var result: [((String, String), String, Int)] = []
|
||||
@ -136,7 +142,9 @@ private func searchCountries(items: [((String, String), String, Int)], query: St
|
||||
let string = "\(item.0) \(item.1)"
|
||||
let tokens = stringTokens(string)
|
||||
if matchStringTokens(tokens, with: queryTokens) {
|
||||
result.append(item)
|
||||
for code in item.2 {
|
||||
result.append((item.0, item.1, code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +166,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode,
|
||||
private let sectionTitles: [String]
|
||||
|
||||
private var searchResults: [((String, String), String, Int)] = []
|
||||
private let countryNamesAndCodes: [((String, String), String, Int)]
|
||||
private let countryNamesAndCodes: [((String, String), String, [Int])]
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) {
|
||||
self.theme = theme
|
||||
@ -181,14 +189,16 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode,
|
||||
self.countryNamesAndCodes = countryNamesAndCodes
|
||||
|
||||
var sections: [(String, [((String, String), String, Int)])] = []
|
||||
for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in
|
||||
for (names, id, codes) in countryNamesAndCodes.sorted(by: { lhs, rhs in
|
||||
return lhs.0.1 < rhs.0.1
|
||||
}) {
|
||||
let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased()
|
||||
if sections.isEmpty || sections[sections.count - 1].0 != title {
|
||||
sections.append((title, []))
|
||||
}
|
||||
sections[sections.count - 1].1.append((names, id, code))
|
||||
for code in codes {
|
||||
sections[sections.count - 1].1.append((names, id, code))
|
||||
}
|
||||
}
|
||||
self.sections = sections
|
||||
self.sectionTitles = sections.map { $0.0 }
|
||||
|
18
submodules/DatePickerNode/BUILD
Normal file
18
submodules/DatePickerNode/BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "DatePickerNode",
|
||||
module_name = "DatePickerNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
388
submodules/DatePickerNode/Sources/DatePickerNode.swift
Normal file
388
submodules/DatePickerNode/Sources/DatePickerNode.swift
Normal file
@ -0,0 +1,388 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let selectedTextFont = Font.bold(13.0)
|
||||
|
||||
public final class DatePickerTheme: Equatable {
|
||||
public let backgroundColor: UIColor
|
||||
public let textColor: UIColor
|
||||
public let secondaryTextColor: UIColor
|
||||
public let accentColor: UIColor
|
||||
public let disabledColor: UIColor
|
||||
public let selectionColor: UIColor
|
||||
public let selectedCurrentTextColor: UIColor
|
||||
public let secondarySelectionColor: UIColor
|
||||
|
||||
public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectedCurrentTextColor: UIColor, secondarySelectionColor: UIColor) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.textColor = textColor
|
||||
self.secondaryTextColor = secondaryTextColor
|
||||
self.accentColor = accentColor
|
||||
self.disabledColor = disabledColor
|
||||
self.selectionColor = selectionColor
|
||||
self.selectedCurrentTextColor = selectedCurrentTextColor
|
||||
self.secondarySelectionColor = secondarySelectionColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool {
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.secondaryTextColor != rhs.secondaryTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionColor != rhs.selectionColor {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedCurrentTextColor != rhs.selectedCurrentTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.secondarySelectionColor != rhs.secondarySelectionColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//public extension DatePickerTheme {
|
||||
// convenience init(theme: PresentationTheme) {
|
||||
// self.init(backgroundColor: theme.rootController.navigationBar.segmentedBackgroundColor, foregroundColor: theme.rootController.navigationBar.segmentedForegroundColor, shadowColor: .black, textColor: theme.rootController.navigationBar.segmentedTextColor, dividerColor: theme.rootController.navigationBar.segmentedDividerColor)
|
||||
// }
|
||||
//}
|
||||
|
||||
private class SegmentedControlItemNode: HighlightTrackingButtonNode {
|
||||
}
|
||||
|
||||
private let telegramReleaseDate = Date(timeIntervalSince1970: 1376438400.0)
|
||||
private let upperLimitDate = Date(timeIntervalSince1970: Double(Int32.max - 1))
|
||||
|
||||
private let dayFont = Font.regular(13.0)
|
||||
private let dateFont = Font.with(size: 13.0, design: .regular, traits: .monospacedNumbers)
|
||||
private let selectedDateFont = Font.bold(13.0)
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
private func monthForDate(_ date: Date) -> Date {
|
||||
var components = calendar.dateComponents([.year, .month], from: date)
|
||||
components.hour = 0
|
||||
components.minute = 0
|
||||
components.second = 0
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
|
||||
public final class DatePickerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
class MonthNode: ASDisplayNode {
|
||||
private let month: Date
|
||||
|
||||
var theme: DatePickerTheme {
|
||||
didSet {
|
||||
if let size = self.validSize {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maximumDate: Date? {
|
||||
didSet {
|
||||
if let size = self.validSize {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var minimumDate: Date? {
|
||||
didSet {
|
||||
if let size = self.validSize {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var date: Date? {
|
||||
didSet {
|
||||
if let size = self.validSize {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validSize: CGSize?
|
||||
|
||||
private let selectionNode: ASImageNode
|
||||
private let dateNodes: [ImmediateTextNode]
|
||||
|
||||
private let firstWeekday: Int
|
||||
private let startWeekday: Int
|
||||
private let numberOfDays: Int
|
||||
|
||||
init(theme: DatePickerTheme, month: Date, minimumDate: Date?, maximumDate: Date?, date: Date?) {
|
||||
self.theme = theme
|
||||
self.month = month
|
||||
self.minimumDate = minimumDate
|
||||
self.maximumDate = maximumDate
|
||||
self.date = date
|
||||
|
||||
self.selectionNode = ASImageNode()
|
||||
self.selectionNode.displaysAsynchronously = false
|
||||
self.selectionNode.displayWithoutProcessing = true
|
||||
|
||||
self.dateNodes = (0..<42).map { _ in ImmediateTextNode() }
|
||||
|
||||
let components = calendar.dateComponents([.year, .month], from: month)
|
||||
let startDayDate = calendar.date(from: components)!
|
||||
|
||||
self.firstWeekday = calendar.firstWeekday
|
||||
self.startWeekday = calendar.dateComponents([.weekday], from: startDayDate).weekday!
|
||||
self.numberOfDays = calendar.range(of: .day, in: .month, for: month)!.count
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.selectionNode)
|
||||
self.dateNodes.forEach { self.addSubnode($0) }
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
var weekday = self.firstWeekday
|
||||
var started = false
|
||||
var count = 0
|
||||
|
||||
for i in 0 ..< 42 {
|
||||
let row: Int = Int(floor(Float(i) / 7.0))
|
||||
let col: Int = i % 7
|
||||
|
||||
if !started && weekday == self.startWeekday {
|
||||
started = true
|
||||
}
|
||||
if started {
|
||||
count += 1
|
||||
|
||||
var isAvailableDate = true
|
||||
if let minimumDate = self.minimumDate {
|
||||
var components = calendar.dateComponents([.year, .month], from: self.month)
|
||||
components.day = count
|
||||
components.hour = 0
|
||||
components.minute = 0
|
||||
let date = calendar.date(from: components)!
|
||||
if date < minimumDate {
|
||||
isAvailableDate = false
|
||||
}
|
||||
}
|
||||
if let maximumDate = self.maximumDate {
|
||||
var components = calendar.dateComponents([.year, .month], from: self.month)
|
||||
components.day = count
|
||||
components.hour = 0
|
||||
components.minute = 0
|
||||
let date = calendar.date(from: components)!
|
||||
if date > maximumDate {
|
||||
isAvailableDate = false
|
||||
}
|
||||
}
|
||||
var isSelectedDate = false
|
||||
var isSelectedAndCurrentDate = false
|
||||
|
||||
let color: UIColor
|
||||
if !isAvailableDate {
|
||||
color = self.theme.disabledColor
|
||||
} else if isSelectedAndCurrentDate {
|
||||
color = .white
|
||||
} else if isSelectedDate {
|
||||
color = self.theme.accentColor
|
||||
} else {
|
||||
color = self.theme.textColor
|
||||
}
|
||||
|
||||
let textNode = self.dateNodes[i]
|
||||
textNode.attributedText = NSAttributedString(string: "\(count)", font: dateFont, textColor: color)
|
||||
|
||||
let textSize = textNode.updateLayout(size)
|
||||
textNode.frame = CGRect(origin: CGPoint(x: CGFloat(col) * 20.0, y: CGFloat(row) * 20.0), size: textSize)
|
||||
|
||||
if count == self.numberOfDays {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
let minDate: Date
|
||||
let maxDate: Date
|
||||
let date: Date
|
||||
|
||||
let displayingMonthSelection: Bool
|
||||
let selectedMonth: Date
|
||||
}
|
||||
|
||||
private var state: State
|
||||
|
||||
private var theme: DatePickerTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let timeTitleNode: ImmediateTextNode
|
||||
private let timeFieldNode: ASImageNode
|
||||
|
||||
private let monthButtonNode: HighlightTrackingButtonNode
|
||||
private let monthTextNode: ImmediateTextNode
|
||||
private let monthArrowNode: ASImageNode
|
||||
|
||||
private let previousButtonNode: HighlightableButtonNode
|
||||
private let nextButtonNode: HighlightableButtonNode
|
||||
|
||||
private let dayNodes: [ImmediateTextNode]
|
||||
private var previousMonthNode: MonthNode?
|
||||
private var currentMonthNode: MonthNode?
|
||||
private var nextMonthNode: MonthNode?
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var gestureRecognizer: UIPanGestureRecognizer?
|
||||
private var gestureSelectedIndex: Int?
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
public var maximumDate: Date? {
|
||||
didSet {
|
||||
|
||||
}
|
||||
}
|
||||
public var minimumDate: Date = telegramReleaseDate {
|
||||
didSet {
|
||||
|
||||
}
|
||||
}
|
||||
public var date: Date = Date() {
|
||||
didSet {
|
||||
guard self.date != oldValue else {
|
||||
return
|
||||
}
|
||||
if let size = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(theme: DatePickerTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.state = State(minDate: telegramReleaseDate, maxDate: upperLimitDate, date: Date(), displayingMonthSelection: false, selectedMonth: monthForDate(Date()))
|
||||
|
||||
self.timeTitleNode = ImmediateTextNode()
|
||||
self.timeFieldNode = ASImageNode()
|
||||
self.timeFieldNode.displaysAsynchronously = false
|
||||
self.timeFieldNode.displayWithoutProcessing = true
|
||||
|
||||
self.monthButtonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.monthTextNode = ImmediateTextNode()
|
||||
|
||||
self.monthArrowNode = ASImageNode()
|
||||
self.monthArrowNode.displaysAsynchronously = false
|
||||
self.monthArrowNode.displayWithoutProcessing = true
|
||||
|
||||
self.previousButtonNode = HighlightableButtonNode()
|
||||
self.nextButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.dayNodes = (0..<7).map { _ in ImmediateTextNode() }
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = theme.backgroundColor
|
||||
|
||||
self.addSubnode(self.monthTextNode)
|
||||
self.addSubnode(self.monthArrowNode)
|
||||
self.addSubnode(self.monthButtonNode)
|
||||
|
||||
self.addSubnode(self.previousButtonNode)
|
||||
self.addSubnode(self.nextButtonNode)
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.scrollNode.view.isPagingEnabled = true
|
||||
self.scrollNode.view.delegate = self
|
||||
}
|
||||
|
||||
private func updateState(_ state: State, animated: Bool) {
|
||||
self.state = state
|
||||
if let size = self.validLayout {
|
||||
self.updateLayout(size: size, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(_ theme: DatePickerTheme) {
|
||||
guard theme != self.theme else {
|
||||
return
|
||||
}
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundColor = self.theme.backgroundColor
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.view.window?.endEditing(true)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
if let size = self.validLayout {
|
||||
self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
if let size = self.validLayout {
|
||||
self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
|
||||
let topInset: CGFloat = 60.0
|
||||
|
||||
let scrollSize = CGSize(width: size.width, height: size.height - topInset)
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: scrollSize)
|
||||
self.scrollNode.view.contentSize = CGSize(width: scrollSize.width * 3.0, height: scrollSize.height)
|
||||
self.scrollNode.view.contentOffset = CGPoint(x: scrollSize.width, y: 0.0)
|
||||
|
||||
for i in 0 ..< self.dayNodes.count {
|
||||
let dayNode = self.dayNodes[i]
|
||||
|
||||
let day = Int32(i)
|
||||
dayNode.attributedText = NSAttributedString(string: shortStringForDayOfWeek(strings: self.strings, day: day), font: dayFont, textColor: theme.secondaryTextColor)
|
||||
let size = dayNode.updateLayout(size)
|
||||
dayNode.frame = CGRect(origin: CGPoint(x: CGFloat(i) * 20.0, y: 0.0), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func monthButtonPressed(_ button: SegmentedControlItemNode) {
|
||||
|
||||
}
|
||||
|
||||
@objc private func previousButtonPressed(_ button: SegmentedControlItemNode) {
|
||||
|
||||
}
|
||||
|
||||
@objc private func nextButtonPressed(_ button: SegmentedControlItemNode) {
|
||||
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import AccountContext
|
||||
|
||||
public enum DeleteChatPeerAction {
|
||||
case delete
|
||||
case deleteAndLeave
|
||||
case clearHistory
|
||||
case clearCache
|
||||
case clearCacheSuggestion
|
||||
@ -57,7 +58,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
let peerFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
|
||||
let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
|
||||
let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0))
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isAccessibilityElement = false
|
||||
@ -93,9 +95,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
case .clearCache, .clearCacheSuggestion:
|
||||
switch action {
|
||||
case .clearCache:
|
||||
attributedText = NSAttributedString(string: strings.ClearCache_Description, font: peerFont, textColor: theme.primaryTextColor)
|
||||
attributedText = NSAttributedString(string: strings.ClearCache_Description, font: textFont, textColor: theme.primaryTextColor)
|
||||
case .clearCacheSuggestion:
|
||||
attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: peerFont, textColor: theme.primaryTextColor)
|
||||
attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: textFont, textColor: theme.primaryTextColor)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -114,6 +116,18 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
} else {
|
||||
text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
}
|
||||
case .deleteAndLeave:
|
||||
if chatPeer.id == context.account.peerId {
|
||||
text = (strings.ChatList_DeleteSavedMessagesConfirmation, [])
|
||||
} else if let chatPeer = chatPeer as? TelegramGroup {
|
||||
text = strings.ChatList_DeleteAndLeaveGroupConfirmation(chatPeer.title)
|
||||
} else if let chatPeer = chatPeer as? TelegramChannel {
|
||||
text = strings.ChatList_DeleteAndLeaveGroupConfirmation(chatPeer.title)
|
||||
} else if chatPeer is TelegramSecretChat {
|
||||
text = strings.ChatList_DeleteSecretChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
} else {
|
||||
text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
}
|
||||
case .clearHistory:
|
||||
text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
case .removeFromGroup:
|
||||
@ -122,9 +136,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
break
|
||||
}
|
||||
if let text = text {
|
||||
var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: peerFont, textColor: theme.primaryTextColor))
|
||||
var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: textFont, textColor: theme.primaryTextColor))
|
||||
for (_, range) in text.1 {
|
||||
formattedAttributedText.addAttribute(.font, value: peerFont, range: range)
|
||||
formattedAttributedText.addAttribute(.font, value: boldFont, range: range)
|
||||
}
|
||||
attributedText = formattedAttributedText
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ public final class ContextMenuContainerNode: ASDisplayNode {
|
||||
let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true)
|
||||
if self.cachedMaskParams != maskParams {
|
||||
let path = UIBezierPath()
|
||||
let cornerRadius: CGFloat = 6.0
|
||||
let cornerRadius: CGFloat = 10.0
|
||||
let verticalInset: CGFloat = 9.0
|
||||
let arrowWidth: CGFloat = 18.0
|
||||
let requestedArrowPosition = maskParams.relativeArrowPosition
|
||||
|
@ -33,7 +33,7 @@ public struct Font {
|
||||
case bold
|
||||
}
|
||||
|
||||
public static func with(size: CGFloat, design: Design = .regular, traits: Traits = []) -> UIFont {
|
||||
public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, traits: Traits = []) -> UIFont {
|
||||
if #available(iOS 13.0, *) {
|
||||
let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor
|
||||
var symbolicTraits = descriptor.symbolicTraits
|
||||
@ -63,6 +63,15 @@ public struct Font {
|
||||
default:
|
||||
updatedDescriptor = updatedDescriptor?.withDesign(.default)
|
||||
}
|
||||
switch weight {
|
||||
case .semibold:
|
||||
let fontTraits = [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]
|
||||
updatedDescriptor = updatedDescriptor?.addingAttributes([
|
||||
UIFontDescriptor.AttributeName.traits: fontTraits
|
||||
])
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let updatedDescriptor = updatedDescriptor {
|
||||
return UIFont(descriptor: updatedDescriptor, size: size)
|
||||
} else {
|
||||
|
@ -497,6 +497,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
var nextItemOrigin = CGPoint(x: initialSpacing + itemInsets.left, y: 0.0)
|
||||
var index = 0
|
||||
var previousSection: GridSection?
|
||||
|
||||
var previousFillsRow = false
|
||||
|
||||
for item in self.items {
|
||||
var itemSize = defaultItemSize
|
||||
|
||||
@ -508,6 +511,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
keepSection = false
|
||||
}
|
||||
|
||||
|
||||
if !previousFillsRow && item.fillsRowWithDynamicHeight != nil {
|
||||
keepSection = false
|
||||
}
|
||||
previousFillsRow = item.fillsRowWithDynamicHeight != nil
|
||||
|
||||
if !keepSection {
|
||||
if incrementedCurrentRow {
|
||||
nextItemOrigin.x = initialSpacing + itemInsets.left
|
||||
|
@ -1351,6 +1351,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
|
||||
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
if let presentingViewController = self.presentingViewController {
|
||||
presentingViewController.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
if let controller = self.presentedViewController {
|
||||
if flag {
|
||||
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: {
|
||||
|
@ -3,10 +3,16 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public protocol TooltipControllerCustomContentNode: ASDisplayNode {
|
||||
func animateIn()
|
||||
func updateLayout(size: CGSize) -> CGSize
|
||||
}
|
||||
|
||||
public enum TooltipControllerContent: Equatable {
|
||||
case text(String)
|
||||
case attributedText(NSAttributedString)
|
||||
case iconAndText(UIImage, String)
|
||||
case custom(TooltipControllerCustomContentNode)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
@ -14,6 +20,8 @@ public enum TooltipControllerContent: Equatable {
|
||||
return text
|
||||
case let .attributedText(text):
|
||||
return text.string
|
||||
case .custom:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +31,35 @@ public enum TooltipControllerContent: Equatable {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public static func == (lhs: TooltipControllerContent, rhs: TooltipControllerContent) -> Bool {
|
||||
switch lhs {
|
||||
case let .text(lhsText):
|
||||
if case let .text(rhsText) = rhs, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .attributedText(lhsText):
|
||||
if case let .attributedText(rhsText) = rhs, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .iconAndText(_, lhsText):
|
||||
if case let .iconAndText(_, rhsText) = rhs, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .custom(lhsNode):
|
||||
if case let .custom(rhsNode) = rhs, lhsNode === rhsNode {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SourceAndRect {
|
||||
|
@ -12,6 +12,7 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
private let containerNode: ContextMenuContainerNode
|
||||
private let imageNode: ASImageNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private var contentNode: TooltipControllerCustomContentNode?
|
||||
|
||||
private let dismissByTapOutside: Bool
|
||||
|
||||
@ -45,10 +46,15 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
|
||||
self.dismiss = dismiss
|
||||
|
||||
if case let .custom(contentNode) = content {
|
||||
self.contentNode = contentNode
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.containerNode.addSubnode(self.imageNode)
|
||||
self.containerNode.addSubnode(self.textNode)
|
||||
self.contentNode.flatMap { self.containerNode.addSubnode($0) }
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
}
|
||||
@ -71,20 +77,37 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
let maxActionsWidth = layout.size.width - 20.0
|
||||
let maxWidth = layout.size.width - 20.0
|
||||
|
||||
var imageSize = CGSize()
|
||||
var imageSizeWithInset = CGSize()
|
||||
if let image = self.imageNode.image {
|
||||
imageSize = image.size
|
||||
imageSizeWithInset = CGSize(width: image.size.width + 12.0, height: image.size.height)
|
||||
let contentSize: CGSize
|
||||
|
||||
if let contentNode = self.contentNode {
|
||||
contentSize = contentNode.updateLayout(size: layout.size)
|
||||
contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
} else {
|
||||
var imageSize = CGSize()
|
||||
var imageSizeWithInset = CGSize()
|
||||
if let image = self.imageNode.image {
|
||||
imageSize = image.size
|
||||
imageSizeWithInset = CGSize(width: image.size.width + 12.0, height: image.size.height)
|
||||
}
|
||||
|
||||
var textSize = self.textNode.updateLayout(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
textSize.width = ceil(textSize.width / 2.0) * 2.0
|
||||
textSize.height = ceil(textSize.height / 2.0) * 2.0
|
||||
|
||||
contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize)
|
||||
if transition.isAnimated, textFrame.size != self.textNode.frame.size {
|
||||
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0))
|
||||
}
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
self.textNode.frame = textFrame
|
||||
}
|
||||
|
||||
var textSize = self.textNode.updateLayout(CGSize(width: maxActionsWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
textSize.width = ceil(textSize.width / 2.0) * 2.0
|
||||
textSize.height = ceil(textSize.height / 2.0) * 2.0
|
||||
let contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0)
|
||||
|
||||
let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize())
|
||||
|
||||
let insets = layout.insets(options: [.statusBar, .input])
|
||||
@ -105,19 +128,11 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom)
|
||||
|
||||
self.containerNode.updateLayout(transition: transition)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize)
|
||||
if transition.isAnimated, textFrame.size != self.textNode.frame.size {
|
||||
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0))
|
||||
}
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
self.textNode.frame = textFrame
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.contentNode?.animateIn()
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
|
@ -139,7 +139,7 @@ public enum TabBarItemContextActionType {
|
||||
}
|
||||
|
||||
open var navigationPresentation: ViewControllerNavigationPresentation = .default
|
||||
var _presentedInModal: Bool = false
|
||||
open var _presentedInModal: Bool = false
|
||||
|
||||
public var presentedOverCoveringView: Bool = false
|
||||
|
||||
|
@ -3,9 +3,6 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public func qewfqewfq() {
|
||||
}
|
||||
|
||||
private struct WindowLayout: Equatable {
|
||||
let size: CGSize
|
||||
let metrics: LayoutMetrics
|
||||
@ -294,7 +291,7 @@ public class Window1 {
|
||||
self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle
|
||||
|
||||
let boundsSize = self.hostView.eventView.bounds.size
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? defaultStatusBarHeight, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? 0.0, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
|
||||
|
||||
self.statusBarHost = statusBarHost
|
||||
let statusBarHeight: CGFloat
|
||||
@ -303,7 +300,7 @@ public class Window1 {
|
||||
self.keyboardManager = KeyboardManager(host: statusBarHost)
|
||||
self.keyboardViewManager = KeyboardViewManager(host: statusBarHost)
|
||||
} else {
|
||||
statusBarHeight = self.deviceMetrics.statusBarHeight
|
||||
statusBarHeight = 0.0
|
||||
self.keyboardManager = nil
|
||||
self.keyboardViewManager = nil
|
||||
}
|
||||
@ -406,7 +403,7 @@ public class Window1 {
|
||||
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate)
|
||||
|
||||
self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarFrameNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, strongSelf.statusBarHost != nil {
|
||||
let statusBarHeight: CGFloat = max(defaultStatusBarHeight, (notification.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? defaultStatusBarHeight)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut)
|
||||
@ -981,10 +978,12 @@ public class Window1 {
|
||||
var statusBarHeight: CGFloat? = self.deviceMetrics.statusBarHeight(for: boundsSize)
|
||||
if let statusBarHeightValue = statusBarHeight, let statusBarHost = self.statusBarHost {
|
||||
statusBarHeight = max(statusBarHeightValue, statusBarHost.statusBarFrame.size.height)
|
||||
} else {
|
||||
statusBarHeight = nil
|
||||
}
|
||||
|
||||
if self.deviceMetrics.type == .tablet, let onScreenNavigationHeight = self.hostView.onScreenNavigationHeight, onScreenNavigationHeight != self.deviceMetrics.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) {
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? defaultStatusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight)
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? 0.0, onScreenNavigationHeight: onScreenNavigationHeight)
|
||||
}
|
||||
|
||||
let statusBarWasHidden = self.statusBarHidden
|
||||
|
@ -491,7 +491,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
|
||||
|
||||
var authorNameText: String?
|
||||
if let author = message.effectiveAuthor {
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
|
||||
authorNameText = authorSignature
|
||||
} else if let author = message.effectiveAuthor {
|
||||
authorNameText = author.displayTitle(strings: self.strings, displayOrder: self.nameOrder)
|
||||
} else if let peer = message.peers[message.id.peerId] {
|
||||
authorNameText = peer.displayTitle(strings: self.strings, displayOrder: self.nameOrder)
|
||||
|
@ -1225,7 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
|
||||
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
|
||||
|
||||
if let (media, _) = mediaForMessage(message: message) {
|
||||
if let _ = mediaForMessage(message: message) {
|
||||
centralItemNode.activateAsInitial()
|
||||
}
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
|
||||
}
|
||||
|
||||
open func setControlsHidden(_ hidden: Bool, animated: Bool) {
|
||||
guard self.areControlsHidden != hidden else {
|
||||
guard self.areControlsHidden != hidden && (!self.isDismissed || hidden) else {
|
||||
return
|
||||
}
|
||||
self.areControlsHidden = hidden
|
||||
|
@ -214,10 +214,10 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
|
||||
case .Local:
|
||||
if let previousStatus = previousStatus, case .Fetching = previousStatus {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusNode.alpha = 0.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
|
||||
|
@ -203,10 +203,10 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
|
||||
case .Local:
|
||||
if let previousStatus = previousStatus, case .Fetching = previousStatus {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusNode.alpha = 0.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
|
||||
|
@ -201,10 +201,10 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
|
||||
case .Local:
|
||||
if let previousStatus = previousStatus, case .Fetching = previousStatus {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusNode.alpha = 0.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
|
||||
|
@ -426,10 +426,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
|
||||
case .Local:
|
||||
if let previousStatus = previousStatus, case .Fetching = previousStatus {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusNode.alpha = 0.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
|
||||
|
@ -723,7 +723,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
var fetching = false
|
||||
if initialBuffering {
|
||||
if displayProgress {
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {})
|
||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {})
|
||||
} else {
|
||||
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
|
||||
}
|
||||
@ -740,7 +740,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
fetching = true
|
||||
isPaused = true
|
||||
}
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true)
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true, animateRotation: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
switch fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Remote:
|
||||
state = .download(.white)
|
||||
default:
|
||||
|
@ -119,7 +119,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
||||
switch fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Remote:
|
||||
state = .download(.white)
|
||||
default:
|
||||
|
56
submodules/InviteLinksUI/BUILD
Normal file
56
submodules/InviteLinksUI/BUILD
Normal file
@ -0,0 +1,56 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "InviteLinksUI",
|
||||
module_name = "InviteLinksUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
|
||||
"//submodules/ItemListPeerItem:ItemListPeerItem",
|
||||
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/SearchUI:SearchUI",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/LegacyUI:LegacyUI",
|
||||
"//submodules/ShareController:ShareController",
|
||||
"//submodules/ContactsPeerItem:ContactsPeerItem",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
"//submodules/TelegramPermissionsUI:TelegramPermissionsUI",
|
||||
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/NotificationSoundSelectionUI:NotificationSoundSelectionUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
|
||||
"//submodules/QrCode:QrCode",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
"//submodules/DatePickerNode:DatePickerNode",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/SectionHeaderItem:SectionHeaderItem",
|
||||
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
477
submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
Normal file
477
submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
Normal file
@ -0,0 +1,477 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import OverlayStatusController
|
||||
import AccountContext
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import TelegramStringFormatting
|
||||
|
||||
private final class InviteLinkEditControllerArguments {
|
||||
let context: AccountContext
|
||||
let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void
|
||||
let dismissInput: () -> Void
|
||||
let revoke: () -> Void
|
||||
|
||||
init(context: AccountContext, updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, dismissInput: @escaping () -> Void, revoke: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.updateState = updateState
|
||||
self.dismissInput = dismissInput
|
||||
self.revoke = revoke
|
||||
}
|
||||
}
|
||||
|
||||
private enum InviteLinksEditSection: Int32 {
|
||||
case time
|
||||
case usage
|
||||
case revoke
|
||||
}
|
||||
|
||||
private let invalidAmountCharacters = CharacterSet(charactersIn: "01234567890.,").inverted
|
||||
func isValidNumberOfUsers(_ number: String) -> Bool {
|
||||
let number = normalizeArabicNumeralString(number, type: .western)
|
||||
if number.rangeOfCharacter(from: invalidAmountCharacters) != nil || number == "0" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
case timeHeader(PresentationTheme, String)
|
||||
case timePicker(PresentationTheme, InviteLinkTimeLimit)
|
||||
case timeExpiryDate(PresentationTheme, Int32?, Bool)
|
||||
case timeCustomPicker(PresentationTheme, Int32?)
|
||||
case timeInfo(PresentationTheme, String)
|
||||
|
||||
case usageHeader(PresentationTheme, String)
|
||||
case usagePicker(PresentationTheme, InviteLinkUsageLimit)
|
||||
case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool)
|
||||
case usageInfo(PresentationTheme, String)
|
||||
|
||||
case revoke(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo:
|
||||
return InviteLinksEditSection.time.rawValue
|
||||
case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo:
|
||||
return InviteLinksEditSection.usage.rawValue
|
||||
case .revoke:
|
||||
return InviteLinksEditSection.revoke.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .timeHeader:
|
||||
return 0
|
||||
case .timePicker:
|
||||
return 1
|
||||
case .timeExpiryDate:
|
||||
return 2
|
||||
case .timeCustomPicker:
|
||||
return 3
|
||||
case .timeInfo:
|
||||
return 4
|
||||
case .usageHeader:
|
||||
return 5
|
||||
case .usagePicker:
|
||||
return 6
|
||||
case .usageCustomPicker:
|
||||
return 7
|
||||
case .usageInfo:
|
||||
return 8
|
||||
case .revoke:
|
||||
return 9
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InviteLinksEditEntry, rhs: InviteLinksEditEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .timeHeader(lhsTheme, lhsText):
|
||||
if case let .timeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timePicker(lhsTheme, lhsValue):
|
||||
if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timeExpiryDate(lhsTheme, lhsDate, lhsActive):
|
||||
if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timeCustomPicker(lhsTheme, lhsDate):
|
||||
if case let .timeCustomPicker(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timeInfo(lhsTheme, lhsText):
|
||||
if case let .timeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usageHeader(lhsTheme, lhsText):
|
||||
if case let .usageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usagePicker(lhsTheme, lhsValue):
|
||||
if case let .usagePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue):
|
||||
if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usageInfo(lhsTheme, lhsText):
|
||||
if case let .usageInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .revoke(lhsTheme, lhsText):
|
||||
if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: InviteLinksEditEntry, rhs: InviteLinksEditEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! InviteLinkEditControllerArguments
|
||||
switch self {
|
||||
case let .timeHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .timePicker(_, value):
|
||||
return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in
|
||||
arguments.updateState({ state in
|
||||
var updatedState = state
|
||||
if value != updatedState.time {
|
||||
updatedState.pickingTimeLimit = false
|
||||
}
|
||||
updatedState.time = value
|
||||
return updatedState
|
||||
})
|
||||
})
|
||||
case let .timeExpiryDate(theme, value, active):
|
||||
let text: String
|
||||
if let value = value {
|
||||
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."))
|
||||
} else {
|
||||
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
|
||||
}
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
|
||||
arguments.dismissInput()
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.pickingTimeLimit = !state.pickingTimeLimit
|
||||
return updatedState
|
||||
}
|
||||
})
|
||||
case let .timeCustomPicker(_, date):
|
||||
return ItemListDatePickerItem(presentationData: presentationData, date: date, sectionId: self.section, style: .blocks, updated: { date in
|
||||
arguments.updateState({ state in
|
||||
var updatedState = state
|
||||
updatedState.time = .custom(date)
|
||||
return updatedState
|
||||
})
|
||||
})
|
||||
case let .timeInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .usageHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .usagePicker(_, value):
|
||||
return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in
|
||||
arguments.dismissInput()
|
||||
arguments.updateState({ state in
|
||||
var updatedState = state
|
||||
if value != updatedState.usage {
|
||||
updatedState.pickingTimeLimit = false
|
||||
}
|
||||
updatedState.usage = value
|
||||
return updatedState
|
||||
})
|
||||
})
|
||||
case let .usageCustomPicker(theme, value, focused, customValue):
|
||||
let text: String
|
||||
if let value = value, value != 0 {
|
||||
text = String(value)
|
||||
} else {
|
||||
text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited
|
||||
}
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, secondaryStyle: !customValue, tag: nil, sectionId: self.section, textUpdated: { updatedText in
|
||||
guard !updatedText.isEmpty else {
|
||||
return
|
||||
}
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.usage = InviteLinkUsageLimit(value: Int32(updatedText))
|
||||
return updatedState
|
||||
}
|
||||
}, shouldUpdateText: { text in
|
||||
return isValidNumberOfUsers(text)
|
||||
}, updatedFocus: { focus in
|
||||
if focus {
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.pickingTimeLimit = false
|
||||
updatedState.pickingUsageLimit = true
|
||||
return updatedState
|
||||
}
|
||||
} else {
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.pickingUsageLimit = false
|
||||
return updatedState
|
||||
}
|
||||
}
|
||||
}, action: {
|
||||
|
||||
})
|
||||
case let .usageInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .revoke(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.revoke()
|
||||
}, tag: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] {
|
||||
var entries: [InviteLinksEditEntry] = []
|
||||
|
||||
entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased()))
|
||||
|
||||
entries.append(.timePicker(presentationData.theme, state.time))
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
var time: Int32?
|
||||
if case let .custom(value) = state.time {
|
||||
time = value
|
||||
} else if let value = state.time.value {
|
||||
time = currentTime + value
|
||||
}
|
||||
entries.append(.timeExpiryDate(presentationData.theme, time, state.pickingTimeLimit))
|
||||
if state.pickingTimeLimit {
|
||||
entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime))
|
||||
}
|
||||
entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
|
||||
|
||||
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
|
||||
entries.append(.usagePicker(presentationData.theme, state.usage))
|
||||
|
||||
var customValue = false
|
||||
if case .custom = state.usage {
|
||||
customValue = true
|
||||
}
|
||||
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue))
|
||||
|
||||
entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
|
||||
|
||||
if let _ = invite {
|
||||
entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private struct InviteLinkEditControllerState: Equatable {
|
||||
var usage: InviteLinkUsageLimit
|
||||
var time: InviteLinkTimeLimit
|
||||
var pickingTimeLimit = false
|
||||
var pickingUsageLimit = false
|
||||
var updating = false
|
||||
}
|
||||
|
||||
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController {
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let initialState: InviteLinkEditControllerState
|
||||
if let invite = invite {
|
||||
var usageLimit = invite.usageLimit
|
||||
if let limit = usageLimit, let count = invite.count, count > 0 {
|
||||
usageLimit = limit - count
|
||||
}
|
||||
|
||||
let timeLimit: InviteLinkTimeLimit
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if let expireDate = invite.expireDate {
|
||||
if currentTime >= expireDate {
|
||||
timeLimit = .day
|
||||
} else {
|
||||
timeLimit = .custom(expireDate)
|
||||
}
|
||||
} else {
|
||||
timeLimit = .unlimited
|
||||
}
|
||||
|
||||
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
} else {
|
||||
initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
}
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
var dismissInputImpl: (() -> Void)?
|
||||
|
||||
let arguments = InviteLinkEditControllerArguments(context: context, updateState: { f in
|
||||
updateState(f)
|
||||
}, dismissInput: {
|
||||
dismissInputImpl?()
|
||||
}, revoke: {
|
||||
guard let invite = invite else {
|
||||
return
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
dismissAction()
|
||||
dismissImpl?()
|
||||
|
||||
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
presentControllerImpl?(controller, nil)
|
||||
})
|
||||
|
||||
let previousState = Atomic<InviteLinkEditControllerState?>(value: nil)
|
||||
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: {
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = true
|
||||
return updatedState
|
||||
}
|
||||
|
||||
let expireDate: Int32?
|
||||
if case let .custom(value) = state.time {
|
||||
expireDate = value
|
||||
} else if let value = state.time.value {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
expireDate = currentTime + value
|
||||
} else {
|
||||
expireDate = 0
|
||||
}
|
||||
|
||||
let usageLimit = state.usage.value
|
||||
if invite == nil {
|
||||
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
dismissImpl?()
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
} else if let invite = invite {
|
||||
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
dismissImpl?()
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let previousState = previousState.swap(state)
|
||||
var animateChanges = false
|
||||
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit {
|
||||
animateChanges = true
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window(.root), with: p)
|
||||
}
|
||||
}
|
||||
dismissInputImpl = { [weak controller] in
|
||||
controller?.view.endEditing(true)
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
return controller
|
||||
}
|
124
submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift
Normal file
124
submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift
Normal file
@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
|
||||
class InviteLinkHeaderItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.sectionId = sectionId
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = InviteLinkHeaderItemNode()
|
||||
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 {
|
||||
guard let nodeValue = node() as? InviteLinkHeaderItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
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 let titleFont = Font.regular(13.0)
|
||||
|
||||
class InviteLinkHeaderItemNode: ListViewItemNode {
|
||||
private let titleNode: TextNode
|
||||
private var animationNode: AnimatedStickerNode
|
||||
|
||||
private var item: InviteLinkHeaderItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 32.0 + params.leftInset
|
||||
let topInset: CGFloat = 92.0
|
||||
|
||||
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.accessibilityLabel = attributedText.string
|
||||
|
||||
let iconSize = CGSize(width: 96.0, height: 96.0)
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.animationNode.updateLayout(size: iconSize)
|
||||
|
||||
let _ = titleApply()
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,747 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import SyncCore
|
||||
import TelegramCore
|
||||
import Display
|
||||
import AccountContext
|
||||
import SolidRoundedButtonNode
|
||||
import ItemListUI
|
||||
import ItemListPeerItem
|
||||
import SectionHeaderItem
|
||||
import TelegramStringFormatting
|
||||
import MergeLists
|
||||
import ContextUI
|
||||
import ShareController
|
||||
import OverlayStatusController
|
||||
import PresentationDataUtils
|
||||
import DirectionalPanGesture
|
||||
import UndoUI
|
||||
|
||||
class InviteLinkInviteInteraction {
|
||||
let context: AccountContext
|
||||
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
|
||||
let copyLink: (ExportedInvitation) -> Void
|
||||
let shareLink: (ExportedInvitation) -> Void
|
||||
let manageLinks: () -> Void
|
||||
|
||||
init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mainLinkContextAction = mainLinkContextAction
|
||||
self.copyLink = copyLink
|
||||
self.shareLink = shareLink
|
||||
self.manageLinks = manageLinks
|
||||
}
|
||||
}
|
||||
|
||||
private struct InviteLinkInviteTransaction {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let isLoading: Bool
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntryId: Hashable {
|
||||
case header
|
||||
case mainLink
|
||||
case links(Int32)
|
||||
case manage
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
case header(PresentationTheme, String, String)
|
||||
case mainLink(PresentationTheme, ExportedInvitation)
|
||||
case links(Int32, PresentationTheme, [ExportedInvitation])
|
||||
case manage(PresentationTheme, String, Bool)
|
||||
|
||||
var stableId: InviteLinkInviteEntryId {
|
||||
switch self {
|
||||
case .header:
|
||||
return .header
|
||||
case .mainLink:
|
||||
return .mainLink
|
||||
case let .links(index, _, _):
|
||||
return .links(index)
|
||||
case .manage:
|
||||
return .manage
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .header(lhsTheme, lhsTitle, lhsText):
|
||||
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .mainLink(lhsTheme, lhsInvitation):
|
||||
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .links(lhsIndex, lhsTheme, lhsInvitations):
|
||||
if case let .links(rhsIndex, rhsTheme, rhsInvitations) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsInvitations == rhsInvitations {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .manage(lhsTheme, lhsText, lhsStandalone):
|
||||
if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .header:
|
||||
switch rhs {
|
||||
case .header:
|
||||
return false
|
||||
case .mainLink, .links, .manage:
|
||||
return true
|
||||
}
|
||||
case .mainLink:
|
||||
switch rhs {
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case .links, .manage:
|
||||
return true
|
||||
}
|
||||
case let .links(lhsIndex, _, _):
|
||||
switch rhs {
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case let .links(rhsIndex, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
case .manage:
|
||||
return true
|
||||
}
|
||||
case .manage:
|
||||
switch rhs {
|
||||
case .header, .mainLink, .links:
|
||||
return false
|
||||
case .manage:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .header(theme, title, text):
|
||||
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
|
||||
case let .mainLink(_, invite):
|
||||
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
|
||||
interaction.copyLink(invite)
|
||||
}, shareAction: {
|
||||
interaction.shareLink(invite)
|
||||
}, contextAction: { node in
|
||||
interaction.mainLinkContextAction(invite, node, nil)
|
||||
}, viewAction: {
|
||||
})
|
||||
case let .links(_, _, invites):
|
||||
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, share: true, sectionId: 1, style: .plain, tapAction: { invite in
|
||||
interaction.copyLink(invite)
|
||||
}, contextAction: { invite, _ in
|
||||
interaction.shareLink(invite)
|
||||
})
|
||||
case let .manage(theme, text, standalone):
|
||||
return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: {
|
||||
interaction.manageLinks()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [InviteLinkInviteEntry], to toEntries: [InviteLinkInviteEntry], isLoading: Bool, account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> InviteLinkInviteTransaction {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return InviteLinkInviteTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
|
||||
}
|
||||
|
||||
public final class InviteLinkInviteController: ViewController {
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private weak var parentNavigationController: NavigationController?
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.parentNavigationController = parentNavigationController
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
}
|
||||
})
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, peerId: self.peerId, controller: self)
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
}
|
||||
|
||||
private var didAppearOnce: Bool = false
|
||||
private var isDismissed: Bool = false
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.didAppearOnce {
|
||||
self.didAppearOnce = true
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
self.didAppearOnce = false
|
||||
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||
private weak var controller: InviteLinkInviteController?
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let invitesContext: PeerExportedInvitationsContext
|
||||
|
||||
private var interaction: InviteLinkInviteInteraction?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let contentNode: ASDisplayNode
|
||||
private let headerNode: ASDisplayNode
|
||||
private let headerBackgroundNode: ASDisplayNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let doneButton: HighlightableButtonNode
|
||||
private let historyBackgroundNode: ASDisplayNode
|
||||
private let historyBackgroundContentNode: ASDisplayNode
|
||||
private var floatingHeaderOffset: CGFloat?
|
||||
private let listNode: ListView
|
||||
|
||||
private var enqueuedTransitions: [InviteLinkInviteTransaction] = []
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private var revokeDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, peerId: PeerId, controller: InviteLinkInviteController) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationDataPromise = Promise(self.presentationData)
|
||||
self.controller = controller
|
||||
|
||||
self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
self.headerNode = ASDisplayNode()
|
||||
self.headerNode.clipsToBounds = true
|
||||
|
||||
self.headerBackgroundNode = ASDisplayNode()
|
||||
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.headerBackgroundNode.cornerRadius = 16.0
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.textAlignment = .center
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
||||
self.doneButton = HighlightableButtonNode()
|
||||
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
|
||||
self.historyBackgroundNode = ASDisplayNode()
|
||||
self.historyBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.historyBackgroundContentNode = ASDisplayNode()
|
||||
self.historyBackgroundContentNode.isLayerBacked = true
|
||||
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
|
||||
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
|
||||
guard let node = node as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
if let invite = invite {
|
||||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
})))
|
||||
|
||||
// items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||
// }, action: { _, f in
|
||||
// f(.dismissWithoutContent)
|
||||
//
|
||||
// if let invite = invite {
|
||||
// let controller = InviteLinkQRCodeController(context: context, invite: invite)
|
||||
// self?.controller?.present(controller, in: .window(.root))
|
||||
// }
|
||||
// })))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
dismissAction()
|
||||
|
||||
self?.revokeDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
}))
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
})))
|
||||
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
self?.controller?.presentInGlobalOverlay(contextController)
|
||||
}, copyLink: { [weak self] invite in
|
||||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}, shareLink: { [weak self] invite in
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
self?.controller?.present(shareController, in: .window(.root))
|
||||
}, manageLinks: { [weak self] in
|
||||
let controller = inviteLinkListController(context: context, peerId: peerId)
|
||||
self?.controller?.parentNavigationController?.pushViewController(controller)
|
||||
self?.controller?.dismiss()
|
||||
})
|
||||
|
||||
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
|
||||
|
||||
let peerView = context.account.postbox.peerView(id: peerId)
|
||||
let invites: Signal<PeerExportedInvitationsState, NoError> = .single(PeerExportedInvitationsState())
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, invites)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
|
||||
if let strongSelf = self {
|
||||
var entries: [InviteLinkInviteEntry] = []
|
||||
|
||||
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
|
||||
|
||||
let mainInvite: ExportedInvitation?
|
||||
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
|
||||
mainInvite = invite
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
|
||||
mainInvite = invite
|
||||
} else {
|
||||
mainInvite = nil
|
||||
}
|
||||
if let mainInvite = mainInvite {
|
||||
entries.append(.mainLink(presentationData.theme, mainInvite))
|
||||
}
|
||||
|
||||
// let additionalInvites = invites.invitations.filter { $0.link != mainInvite?.link }
|
||||
// var index: Int32 = 0
|
||||
// for i in stride(from: 0, to: additionalInvites.endIndex, by: 2) {
|
||||
// var invitesPair: [ExportedInvitation] = []
|
||||
// invitesPair.append(additionalInvites[i])
|
||||
// if i + 1 < additionalInvites.count {
|
||||
// invitesPair.append(additionalInvites[i + 1])
|
||||
// }
|
||||
// entries.append(.links(index, presentationData.theme, invitesPair))
|
||||
// index += 1
|
||||
// }
|
||||
|
||||
// entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, additionalInvites.isEmpty))
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
|
||||
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
})
|
||||
|
||||
self.listNode.preloadPages = true
|
||||
self.listNode.stackFromBottom = true
|
||||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
|
||||
}
|
||||
}
|
||||
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
|
||||
if case let .known(value) = offset, value < 40.0 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.contentNode)
|
||||
self.contentNode.addSubnode(self.historyBackgroundNode)
|
||||
self.contentNode.addSubnode(self.listNode)
|
||||
self.contentNode.addSubnode(self.headerNode)
|
||||
|
||||
self.headerNode.addSubnode(self.headerBackgroundNode)
|
||||
self.headerNode.addSubnode(self.doneButton)
|
||||
|
||||
self.doneButton.addTarget(self, action: #selector(self.doneButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings {
|
||||
strongSelf.updatePresentationData(presentationData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.revokeDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
@objc private func doneButtonPressed() {
|
||||
self.controller?.dismiss()
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
self.presentationDataPromise.set(.single(presentationData))
|
||||
|
||||
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: InviteLinkInviteTransaction) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
if let _ = self.validLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
|
||||
return
|
||||
}
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: ListViewDeleteAndInsertOptions(), updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
|
||||
let initialBounds = self.contentNode.bounds
|
||||
self.contentNode.bounds = initialBounds.offsetBy(dx: 0.0, dy: -layout.size.height)
|
||||
transition.animateView({
|
||||
self.contentNode.view.bounds = initialBounds
|
||||
})
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)?) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
var offsetCompleted = false
|
||||
let internalCompletion: () -> Void = {
|
||||
if offsetCompleted {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
self.contentNode.layer.animateBoundsOriginYAdditive(from: self.contentNode.bounds.origin.y, to: -layout.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
offsetCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.left = layout.safeInsets.left
|
||||
insets.right = layout.safeInsets.right
|
||||
insets.bottom = layout.intrinsicInsets.bottom
|
||||
|
||||
let headerHeight: CGFloat = 54.0
|
||||
let visibleItemsHeight: CGFloat = 409.0
|
||||
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
|
||||
let listTopInset = layoutTopInset + headerHeight
|
||||
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
|
||||
|
||||
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight - insets.bottom)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize))
|
||||
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 68.0))
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
let doneSize = self.doneButton.measure(CGSize(width: layout.size.width, height: headerHeight))
|
||||
let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - doneSize.width - 16.0, y: 18.0), size: doneSize)
|
||||
transition.updateFrame(node: self.doneButton, frame: doneFrame)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
|
||||
if result === self.headerNode.view {
|
||||
return self.view
|
||||
}
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
if point.y < self.headerNode.frame.minY {
|
||||
return self.dimNode.view
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.controller?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private var panGestureArguments: CGFloat?
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return gestureRecognizer is DirectionalPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
let contentOffset = self.listNode.visibleContentOffset()
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.panGestureArguments = 0.0
|
||||
case .changed:
|
||||
var translation = recognizer.translation(in: self.contentNode.view).y
|
||||
if let currentOffset = self.panGestureArguments {
|
||||
if case let .known(value) = contentOffset, value <= 0.5 {
|
||||
if currentOffset > 0.0 {
|
||||
let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller)
|
||||
if translation.y > 10.0 {
|
||||
self.listNode.scroller.panGestureRecognizer.isEnabled = false
|
||||
self.listNode.scroller.panGestureRecognizer.isEnabled = true
|
||||
} else {
|
||||
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
translation = 0.0
|
||||
recognizer.setTranslation(CGPoint(), in: self.contentNode.view)
|
||||
}
|
||||
|
||||
self.panGestureArguments = translation
|
||||
}
|
||||
|
||||
var bounds = self.contentNode.bounds
|
||||
bounds.origin.y = -translation
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
self.contentNode.bounds = bounds
|
||||
case .ended:
|
||||
let translation = recognizer.translation(in: self.contentNode.view)
|
||||
var velocity = recognizer.velocity(in: self.contentNode.view)
|
||||
|
||||
if case let .known(value) = contentOffset, value > 0.0 {
|
||||
velocity = CGPoint()
|
||||
} else if case .unknown = contentOffset {
|
||||
velocity = CGPoint()
|
||||
}
|
||||
|
||||
var bounds = self.contentNode.bounds
|
||||
bounds.origin.y = -translation.y
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
|
||||
self.panGestureArguments = nil
|
||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
|
||||
self.controller?.dismiss()
|
||||
} else {
|
||||
var bounds = self.contentNode.bounds
|
||||
let previousBounds = bounds
|
||||
bounds.origin.y = 0.0
|
||||
self.contentNode.bounds = bounds
|
||||
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
case .cancelled:
|
||||
self.panGestureArguments = nil
|
||||
|
||||
let previousBounds = self.contentNode.bounds
|
||||
var bounds = self.contentNode.bounds
|
||||
bounds.origin.y = 0.0
|
||||
self.contentNode.bounds = bounds
|
||||
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
self.floatingHeaderOffset = offset
|
||||
|
||||
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
|
||||
|
||||
let controlsHeight: CGFloat = 44.0
|
||||
|
||||
let listTopInset = layoutTopInset + controlsHeight
|
||||
|
||||
let rawControlsOffset = offset + listTopInset - controlsHeight
|
||||
let controlsOffset = max(layoutTopInset, rawControlsOffset)
|
||||
let isOverscrolling = rawControlsOffset <= layoutTopInset
|
||||
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
|
||||
|
||||
let previousFrame = self.headerNode.frame
|
||||
|
||||
if !controlsFrame.equalTo(previousFrame) {
|
||||
self.headerNode.frame = controlsFrame
|
||||
|
||||
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
|
||||
|
||||
transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y)
|
||||
}
|
||||
|
||||
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
|
||||
|
||||
let previousBackgroundFrame = self.historyBackgroundNode.frame
|
||||
|
||||
if !backgroundFrame.equalTo(previousBackgroundFrame) {
|
||||
self.historyBackgroundNode.frame = backgroundFrame
|
||||
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
|
||||
|
||||
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
|
||||
|
||||
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
|
||||
class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
|
||||
var sectionId: ItemListSectionId = 0
|
||||
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
init(theme: PresentationTheme, title: String, text: String) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = InviteLinkInviteHeaderItemNode()
|
||||
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 {
|
||||
guard let nodeValue = node() as? InviteLinkInviteHeaderItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
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 let titleFont = Font.medium(23.0)
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextNode
|
||||
private let iconBackgroundNode: ASImageNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
private var item: InviteLinkInviteHeaderItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconBackgroundNode = ASImageNode()
|
||||
self.iconBackgroundNode.displaysAsynchronously = false
|
||||
self.iconBackgroundNode.displayWithoutProcessing = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.contentMode = .center
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.iconBackgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 40.0 + params.leftInset
|
||||
let topInset: CGFloat = 98.0
|
||||
let spacing: CGFloat = 8.0
|
||||
let bottomInset: CGFloat = 24.0
|
||||
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + spacing + textLayout.size.height + bottomInset)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.accessibilityLabel = attributedText.string
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
|
||||
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 92.0, height: 92.0)
|
||||
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
|
||||
|
||||
let _ = titleApply()
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
|
||||
|
||||
let _ = textApply()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
|
||||
class InviteLinkInviteManageItem: ListViewItem, ItemListItem {
|
||||
var sectionId: ItemListSectionId = 0
|
||||
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let standalone: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(theme: PresentationTheme, text: String, standalone: Bool, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.standalone = standalone
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = InviteLinkInviteManageItemNode()
|
||||
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 {
|
||||
guard let nodeValue = node() as? InviteLinkInviteManageItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
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 let titleFont = Font.medium(23.0)
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
class InviteLinkInviteManageItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
|
||||
private var item: InviteLinkInviteManageItem?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.item?.action()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InviteLinkInviteManageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
return { item, params, neighbors in
|
||||
let contentSize = CGSize(width: params.width, height: 70.0)
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.backgroundNode.backgroundColor = item.standalone ? .clear : item.theme.list.blocksBackgroundColor
|
||||
|
||||
strongSelf.buttonNode.setTitle(item.text, with: Font.regular(17.0), with: item.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: 1000.0))
|
||||
|
||||
let size = strongSelf.buttonNode.measure(layout.contentSize)
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.contentSize.width - size.width) / 2.0), y: floorToScreenPixels((layout.contentSize.height - size.height) / 2.0)), size: size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
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)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user