Merge branch 'master' into experimental-2

This commit is contained in:
Ali 2021-01-28 17:32:27 +05:00
commit 1631103633
422 changed files with 39126 additions and 10449 deletions

171
Makefile
View File

@ -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
View File

@ -1,16 +1,108 @@
# Telegram iOS Source Code Compilation Guide
1. Install the brew package manager, if you havent 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.

View File

@ -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",

View File

@ -9,6 +9,7 @@ import Contacts
import OpenSSLEncryptionProvider
import AppLockState
import UIKit
import GeneratedSources
private var accountCache: Account?

Binary file not shown.

Binary file not shown.

View 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&apos;ll be there in 10 minutes</string>
</array>
</dict>
</array>
</dict>
</plist>

View 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 для разблакіравання праграмы.";

View File

@ -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";

View File

@ -2,11 +2,11 @@
"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства.";
"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. ";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями.";
"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch.";
"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео.";
"NSPhotoLibraryUsageDescription" = "Это необходимо, чтобы вы могли делиться фото и видео из библиотеки устройства.";
"NSPhotoLibraryAddUsageDescription" = "Это необходимо, чтобы вы могли сохранять фото и видео в библиотеку устройства.";
"NSMicrophoneUsageDescription" = "Это необходимо, чтобы вы могли делиться голосовыми сообщениями и видео со звуком.";
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений";
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений.";
"NSFaceIDUsageDescription" = "Вы можете разблокировать приложение с помощью Face ID.";

View File

@ -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)
}

View 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
View 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

View 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])

View File

@ -1 +0,0 @@
3.7.0

View File

View File

@ -0,0 +1,11 @@
exports_files([
"Intents.mobileprovision",
"NotificationContent.mobileprovision",
"NotificationService.mobileprovision",
"Share.mobileprovision",
"Telegram.mobileprovision",
"WatchApp.mobileprovision",
"WatchExtension.mobileprovision",
"Widget.mobileprovision",
])

View 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"

View File

@ -0,0 +1,7 @@
#!/bin/sh
if [ ! -d "$1" ]; then
exit 1
fi
cp -R build-system/example-configuration/* "$1/"

View File

@ -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"

View File

@ -1 +0,0 @@
12.2

View File

@ -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"

View File

@ -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"

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -1,5 +1,5 @@
load(
"//build-input/data:variables.bzl",
"@build_configuration//:variables.bzl",
"telegram_api_id",
"telegram_api_hash",
"telegram_app_center_id",

View File

@ -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) {

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/MergeLists:MergeLists",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/ContextUI:ContextUI",
],
visibility = [
"//visibility:public",

View File

@ -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))
}

View File

@ -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, {

View File

@ -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))

View File

@ -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
}
}

View 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)
}
}

View 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",
],
)

View File

@ -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)
}
}

View 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",
],
)

View 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()
}
}
}
}
}

View File

@ -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 {

View File

@ -58,6 +58,7 @@ swift_library(
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/StickerResources:StickerResources",
],
visibility = [
"//visibility:public",

View File

@ -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
/*super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)*/
if let item = self.item, case .action = item.appearance {
super.setHighlighted(highlighted, at: point, animated: animated)
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
let selectionNode: CheckNode
if let current = currentSelectionNode {
selectionNode = current
updatedSelectionNode = selectionNode
if case .option = item.appearance {
rightInset += 28.0
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))

View File

@ -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
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()
}
})
}
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()
}
})
}
}))
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
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(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
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))
}
}))
}
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
if chatPeer is TelegramSecretChat {
beginClear(.forEveryone)
} else {
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, 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()
}

View File

@ -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)?

View File

@ -684,7 +684,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers)
|> 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
@ -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)

View File

@ -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

View File

@ -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:

View File

@ -129,9 +129,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private var pendingPanes: [ChatListSearchPaneKey: ChatListSearchPendingPane] = [:]
private var transitionFraction: CGFloat = 0.0
var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)?
var requestExpandTabs: (() -> Bool)?

View File

@ -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 {

View File

@ -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:

View File

@ -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)
}
})
})

View File

@ -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
}
}

View File

@ -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: {})
}
}

View File

@ -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)
}

View File

@ -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
}

View 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",
],
)

View File

@ -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")
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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()
})
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
if transition.isAnimated {
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)
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 {

View File

@ -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 }

View 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",
],
)

View 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) {
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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: {

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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:

View File

@ -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:

View 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",
],
)

View 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
}

View 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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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