diff --git a/.bazelversion b/.bazelversion index ef09838cb29..34a8f745d4e 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.1.1 \ No newline at end of file +7.3.1 \ No newline at end of file diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 01e2a67b4d2..622f54ede95 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -13,6 +13,14 @@ jobs: runs-on: macos-14 steps: + - uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.SSH_KEY }} + + - name: Add SSH known hosts + run: | + ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts + - uses: actions/checkout@v2 with: submodules: 'recursive' @@ -31,14 +39,8 @@ jobs: cp -R $GITHUB_WORKSPACE /Users/telegram/ mv /Users/telegram/$(basename $GITHUB_WORKSPACE) /Users/telegram/telegram-ios - - uses: webfactory/ssh-agent@v0.5.4 - with: - ssh-private-key: ${{ secrets.SSH_KEY }} - - name: New testflight build run: | - ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts - set -x mkdir -p $BUILD_WORKING_DIR diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index f2924022783..0edd01e16d4 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -10,6 +10,14 @@ jobs: runs-on: macos-14 steps: + - uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.SSH_KEY }} + + - name: Add SSH known hosts + run: | + ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts + - uses: actions/checkout@v2 with: submodules: 'recursive' @@ -28,14 +36,8 @@ jobs: cp -R $GITHUB_WORKSPACE /Users/telegram/ mv /Users/telegram/$(basename $GITHUB_WORKSPACE) /Users/telegram/telegram-ios - - uses: webfactory/ssh-agent@v0.5.4 - with: - ssh-private-key: ${{ secrets.SSH_KEY }} - - name: New testflight build run: | - ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts - set -x mkdir -p $BUILD_WORKING_DIR diff --git a/.gitignore b/.gitignore index 9496c41aec0..b983532880d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,9 @@ ci/working_dir .build Telegram/Telegram-iOS/FirebaseRemoteConfigDefaults.plist *telegram-bazel-cache* -Package_local.swift \ No newline at end of file +Package_local.swift +submodules/OpusBinding/SharedHeaders/* +submodules/FFMpegBinding/SharedHeaders/* +submodules/OpenSSLEncryptionProvider/SharedHeaders/* + + diff --git a/.gitmodules b/.gitmodules index 38d4aca6a50..91273bc149d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,7 +15,7 @@ url = https://github.com/telegramdesktop/libtgvoip.git [submodule "submodules/TgVoipWebrtc/tgcalls"] path = submodules/TgVoipWebrtc/tgcalls - url = https://github.com/TelegramMessenger/tgcalls.git + url = git@bitbucket.org:mobyrix/tgcalls.git [submodule "third-party/libvpx/libvpx"] path = third-party/libvpx/libvpx url = https://github.com/webmproject/libvpx.git @@ -26,7 +26,7 @@ # Perhaps the crash is related to the minimum version of iOS (we have iOS 14, telegram has iOS 12). [submodule "third-party/webrtc/webrtc"] path = third-party/webrtc/webrtc - url = https://github.com/denis15yo/webrtc.git + url = https://github.com/nicegram/webrtc.git [submodule "third-party/libx264/x264"] path = third-party/libx264/x264 url = https://github.com/mirror/x264.git diff --git a/MODULE.bazel b/MODULE.bazel index 26b5be72875..d4a0395b8a2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ bazel_dep( name = "rules_swift_package_manager", - version = "0.36.0", + version = "0.39.0", ) bazel_dep( name = "apple_support", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index e03879c15e6..1ac82d710e9 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,1848 +1,104 @@ { - "lockFileVersion": 6, - "moduleFileHash": "731e3e6cdd2607a65a47bc04d153a84b9fc1b61b3ede7d070a1396e0ec0059a3", - "flags": { - "cmdRegistries": [ - "https://bcr.bazel.build/" - ], - "cmdModuleOverrides": {}, - "allowedYankedVersions": [], - "envVarAllowedYankedVersions": "", - "ignoreDevDependency": false, - "directDependenciesMode": "WARNING", - "compatibilityMode": "ERROR" - }, - "localOverrideHashes": { - "rules_apple": "cd087b86f43819d8413b130a554f8ea919826efa9d436218191abc5edf68fe4c", - "apple_support": "8cfbef5c82204246df4ae09be81908e8c48495037c2aac960109136a288caec3", - "bazel_tools": "1ae69322ac3823527337acf02016e8ee95813d8d356f47060255b8956fa642f0", - "rules_swift": "e2ca418271aade7e3c320c6f0163c477ae82a28a46126db80a046ede5e296c69", - "rules_xcodeproj": "6fbfbadd9e80dc8ad14c50e21e794c090fd7f7f528ed9b78feec3d2daa4eaa25" - }, - "moduleDepGraph": { - "": { - "name": "", - "version": "", - "key": "", - "repoName": "", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_swift_package_manager//:extensions.bzl", - "extensionName": "swift_deps", - "usingModule": "", - "location": { - "file": "@@//:MODULE.bazel", - "line": 44, - "column": 27 - }, - "imports": { - "swiftpkg_nicegram_assistant_ios": "swiftpkg_nicegram_assistant_ios", - "swiftpkg_nicegram_wallet_ios": "swiftpkg_nicegram_wallet_ios" - }, - "devImports": [], - "tags": [ - { - "tagName": "from_package", - "attributeValues": { - "resolved": "//:Package.resolved", - "swift": "//:Package.swift" - }, - "devDependency": false, - "location": { - "file": "@@//:MODULE.bazel", - "line": 48, - "column": 24 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "rules_swift_package_manager": "rules_swift_package_manager@0.36.0", - "apple_support": "apple_support@_", - "build_bazel_rules_swift": "rules_swift@_", - "build_bazel_rules_apple": "rules_apple@_", - "rules_xcodeproj": "rules_xcodeproj@_", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - } - }, - "rules_swift_package_manager@0.36.0": { - "name": "rules_swift_package_manager", - "version": "0.36.0", - "key": "rules_swift_package_manager@0.36.0", - "repoName": "rules_swift_package_manager", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@apple_support//crosstool:setup.bzl", - "extensionName": "apple_cc_configure_extension", - "usingModule": "rules_swift_package_manager@0.36.0", - "location": { - "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", - "line": 40, - "column": 35 - }, - "imports": { - "local_config_apple_cc": "local_config_apple_cc" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", - "extensionName": "go_deps", - "usingModule": "rules_swift_package_manager@0.36.0", - "location": { - "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", - "line": 46, - "column": 24 - }, - "imports": { - "com_github_bazelbuild_buildtools": "com_github_bazelbuild_buildtools", - "com_github_creasty_defaults": "com_github_creasty_defaults", - "com_github_deckarep_golang_set_v2": "com_github_deckarep_golang_set_v2", - "com_github_spf13_cobra": "com_github_spf13_cobra", - "com_github_stretchr_testify": "com_github_stretchr_testify", - "in_gopkg_yaml_v3": "in_gopkg_yaml_v3", - "org_golang_x_exp": "org_golang_x_exp", - "org_golang_x_text": "org_golang_x_text" - }, - "devImports": [], - "tags": [ - { - "tagName": "from_file", - "attributeValues": { - "go_mod": "//:go.mod" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", - "line": 47, - "column": 18 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "cgrindel_bazel_starlib": "cgrindel_bazel_starlib@0.21.0", - "bazel_skylib": "bazel_skylib@1.5.0", - "io_bazel_rules_go": "rules_go@0.47.0", - "apple_support": "apple_support@_", - "rules_cc": "rules_cc@0.0.9", - "platforms": "platforms@0.0.9", - "build_bazel_rules_swift": "rules_swift@_", - "build_bazel_rules_apple": "rules_apple@_", - "bazel_gazelle": "gazelle@0.37.0", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/cgrindel/rules_swift_package_manager/releases/download/v0.36.0/rules_swift_package_manager.v0.36.0.tar.gz" - ], - "integrity": "sha256-3m5u/pXNxFQAKuGvgAARQSnpVPBMYFgFVn/lvG5xV94=", - "strip_prefix": "", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/patches/module_dot_bazel_version.patch": "sha256-ivKQmiiCNC9DFlpC53UKdfdVrwRJsZCEpMp9x+Xsu28=" - }, - "remote_patch_strip": 1 - } - } - }, - "apple_support@_": { - "name": "apple_support", - "version": "0", - "key": "apple_support@_", - "repoName": "build_bazel_apple_support", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@local_config_apple_cc_toolchains//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", - "extensionName": "apple_cc_configure_extension", - "usingModule": "apple_support@_", - "location": { - "file": "@@apple_support~//:MODULE.bazel", - "line": 19, - "column": 35 - }, - "imports": { - "local_config_apple_cc": "local_config_apple_cc", - "local_config_apple_cc_toolchains": "local_config_apple_cc_toolchains" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - } - }, - "rules_swift@_": { - "name": "rules_swift", - "version": "0", - "key": "rules_swift@_", - "repoName": "build_bazel_rules_swift", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@build_bazel_rules_swift//swift:extensions.bzl", - "extensionName": "non_module_deps", - "usingModule": "rules_swift@_", - "location": { - "file": "@@rules_swift~//:MODULE.bazel", - "line": 18, - "column": 32 - }, - "imports": { - "build_bazel_rules_swift_index_import": "build_bazel_rules_swift_index_import", - "build_bazel_rules_swift_local_config": "build_bazel_rules_swift_local_config", - "com_github_apple_swift_log": "com_github_apple_swift_log", - "com_github_apple_swift_nio": "com_github_apple_swift_nio", - "com_github_apple_swift_nio_extras": "com_github_apple_swift_nio_extras", - "com_github_apple_swift_nio_http2": "com_github_apple_swift_nio_http2", - "com_github_apple_swift_nio_transport_services": "com_github_apple_swift_nio_transport_services", - "com_github_apple_swift_protobuf": "com_github_apple_swift_protobuf", - "com_github_grpc_grpc_swift": "com_github_grpc_grpc_swift" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", - "extensionName": "apple_cc_configure_extension", - "usingModule": "rules_swift@_", - "location": { - "file": "@@rules_swift~//:MODULE.bazel", - "line": 32, - "column": 35 - }, - "imports": { - "local_config_apple_cc": "local_config_apple_cc" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_features": "bazel_features@1.9.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "build_bazel_apple_support": "apple_support@_", - "rules_cc": "rules_cc@0.0.9", - "platforms": "platforms@0.0.9", - "com_google_protobuf": "protobuf@21.7", - "rules_proto": "rules_proto@5.3.0-21.7", - "com_github_nlohmann_json": "nlohmann_json@3.6.1", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - } - }, - "rules_apple@_": { - "name": "rules_apple", - "version": "0", - "key": "rules_apple@_", - "repoName": "build_bazel_rules_apple", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@build_bazel_rules_apple//apple:extensions.bzl", - "extensionName": "non_module_deps", - "usingModule": "rules_apple@_", - "location": { - "file": "@@rules_apple~//:MODULE.bazel", - "line": 21, - "column": 32 - }, - "imports": { - "xctestrunner": "xctestrunner" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@build_bazel_rules_apple//apple:apple.bzl", - "extensionName": "provisioning_profile_repository_extension", - "usingModule": "rules_apple@_", - "location": { - "file": "@@rules_apple~//:MODULE.bazel", - "line": 27, - "column": 48 - }, - "imports": { - "local_provisioning_profiles": "local_provisioning_profiles" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", - "extensionName": "apple_cc_configure_extension", - "usingModule": "rules_apple@_", - "location": { - "file": "@@rules_apple~//:MODULE.bazel", - "line": 30, - "column": 35 - }, - "imports": { - "local_config_apple_cc": "local_config_apple_cc" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "build_bazel_apple_support": "apple_support@_", - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.9", - "build_bazel_rules_swift": "rules_swift@_", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - } - }, - "rules_xcodeproj@_": { - "name": "rules_xcodeproj", - "version": "0.0.0", - "key": "rules_xcodeproj@_", - "repoName": "rules_xcodeproj", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_xcodeproj//xcodeproj:extensions.bzl", - "extensionName": "internal", - "usingModule": "rules_xcodeproj@_", - "location": { - "file": "@@rules_xcodeproj~//:MODULE.bazel", - "line": 23, - "column": 25 - }, - "imports": { - "rules_xcodeproj_generated": "rules_xcodeproj_generated" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@rules_xcodeproj//xcodeproj:extensions.bzl", - "extensionName": "non_module_deps", - "usingModule": "rules_xcodeproj@_", - "location": { - "file": "@@rules_xcodeproj~//:MODULE.bazel", - "line": 26, - "column": 32 - }, - "imports": { - "rules_xcodeproj_index_import": "rules_xcodeproj_index_import", - "com_github_apple_swift_argument_parser": "com_github_apple_swift_argument_parser", - "com_github_apple_swift_collections": "com_github_apple_swift_collections", - "com_github_kylef_pathkit": "com_github_kylef_pathkit", - "com_github_michaeleisel_jjliso8601dateformatter": "com_github_michaeleisel_jjliso8601dateformatter", - "com_github_michaeleisel_zippyjson": "com_github_michaeleisel_zippyjson", - "com_github_michaeleisel_zippyjsoncfamily": "com_github_michaeleisel_zippyjsoncfamily", - "com_github_tadija_aexml": "com_github_tadija_aexml", - "com_github_tuist_xcodeproj": "com_github_tuist_xcodeproj" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_features": "bazel_features@1.9.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "build_bazel_rules_swift": "rules_swift@_", - "build_bazel_rules_apple": "rules_apple@_", - "rules_python": "rules_python@0.27.1", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - } - }, - "bazel_tools@_": { - "name": "bazel_tools", - "version": "", - "key": "bazel_tools@_", - "repoName": "bazel_tools", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@local_config_cc_toolchains//:all", - "@local_config_sh//:local_sh_toolchain" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@bazel_tools//tools/cpp:cc_configure.bzl", - "extensionName": "cc_configure_extension", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 18, - "column": 29 - }, - "imports": { - "local_config_cc": "local_config_cc", - "local_config_cc_toolchains": "local_config_cc_toolchains" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_tools//tools/osx:xcode_configure.bzl", - "extensionName": "xcode_configure_extension", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 22, - "column": 32 - }, - "imports": { - "local_config_xcode": "local_config_xcode" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@rules_java//java:extensions.bzl", - "extensionName": "toolchains", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 25, - "column": 32 - }, - "imports": { - "local_jdk": "local_jdk", - "remote_java_tools": "remote_java_tools", - "remote_java_tools_linux": "remote_java_tools_linux", - "remote_java_tools_windows": "remote_java_tools_windows", - "remote_java_tools_darwin_x86_64": "remote_java_tools_darwin_x86_64", - "remote_java_tools_darwin_arm64": "remote_java_tools_darwin_arm64" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_tools//tools/sh:sh_configure.bzl", - "extensionName": "sh_configure_extension", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 36, - "column": 39 - }, - "imports": { - "local_config_sh": "local_config_sh" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_tools//tools/test:extensions.bzl", - "extensionName": "remote_coverage_tools_extension", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 40, - "column": 48 - }, - "imports": { - "remote_coverage_tools": "remote_coverage_tools" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_tools//tools/android:android_extensions.bzl", - "extensionName": "remote_android_tools_extensions", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 43, - "column": 42 - }, - "imports": { - "android_gmaven_r8": "android_gmaven_r8", - "android_tools": "android_tools" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@buildozer//:buildozer_binary.bzl", - "extensionName": "buildozer_binary", - "usingModule": "bazel_tools@_", - "location": { - "file": "@@bazel_tools//:MODULE.bazel", - "line": 47, - "column": 33 - }, - "imports": { - "buildozer_binary": "buildozer_binary" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "rules_cc": "rules_cc@0.0.9", - "rules_java": "rules_java@7.4.0", - "rules_license": "rules_license@0.0.7", - "rules_proto": "rules_proto@5.3.0-21.7", - "rules_python": "rules_python@0.27.1", - "buildozer": "buildozer@6.4.0.2", - "platforms": "platforms@0.0.9", - "com_google_protobuf": "protobuf@21.7", - "zlib": "zlib@1.3", - "build_bazel_apple_support": "apple_support@_", - "local_config_platform": "local_config_platform@_" - } - }, - "local_config_platform@_": { - "name": "local_config_platform", - "version": "", - "key": "local_config_platform@_", - "repoName": "local_config_platform", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_" - } - }, - "cgrindel_bazel_starlib@0.21.0": { - "name": "cgrindel_bazel_starlib", - "version": "0.21.0", - "key": "cgrindel_bazel_starlib@0.21.0", - "repoName": "cgrindel_bazel_starlib", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", - "extensionName": "go_deps", - "usingModule": "cgrindel_bazel_starlib@0.21.0", - "location": { - "file": "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/MODULE.bazel", - "line": 31, - "column": 24 - }, - "imports": { - "com_github_creasty_defaults": "com_github_creasty_defaults", - "com_github_gomarkdown_markdown": "com_github_gomarkdown_markdown", - "com_github_stretchr_testify": "com_github_stretchr_testify", - "in_gopkg_yaml_v3": "in_gopkg_yaml_v3" - }, - "devImports": [], - "tags": [ - { - "tagName": "from_file", - "attributeValues": { - "go_mod": "//:go.mod" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/MODULE.bazel", - "line": 32, - "column": 18 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "io_bazel_rules_go": "rules_go@0.47.0", - "bazel_gazelle": "gazelle@0.37.0", - "bazel_skylib": "bazel_skylib@1.5.0", - "io_bazel_stardoc": "stardoc@0.6.2", - "buildifier_prebuilt": "buildifier_prebuilt@6.0.0.1", - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/cgrindel/bazel-starlib/releases/download/v0.21.0/bazel-starlib.v0.21.0.tar.gz" - ], - "integrity": "sha256-Q+N1IT2r4MOSjmVBLqfsFoUNuTKFyMb4sOqkHKzQ+II=", - "strip_prefix": "", - "remote_patches": { - "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/patches/module_dot_bazel_version.patch": "sha256-hKDFxjpoeavbJZK7KL182NpKl3Mm2UU89KFdgKt84eI=" - }, - "remote_patch_strip": 1 - } - } - }, - "bazel_skylib@1.5.0": { - "name": "bazel_skylib", - "version": "1.5.0", - "key": "bazel_skylib@1.5.0", - "repoName": "bazel_skylib", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "//toolchains/unittest:cmd_toolchain", - "//toolchains/unittest:bash_toolchain" - ], - "extensionUsages": [], - "deps": { - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" - ], - "integrity": "sha256-zVWgYudjuTSZIfD124w5MyiNyLpPdt2UFqrGis7jy5Q=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "rules_go@0.47.0": { - "name": "rules_go", - "version": "0.47.0", - "key": "rules_go@0.47.0", - "repoName": "io_bazel_rules_go", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@go_toolchains//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@io_bazel_rules_go//go:extensions.bzl", - "extensionName": "go_sdk", - "usingModule": "rules_go@0.47.0", - "location": { - "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", - "line": 16, - "column": 23 - }, - "imports": { - "go_toolchains": "go_toolchains", - "io_bazel_rules_nogo": "io_bazel_rules_nogo" - }, - "devImports": [], - "tags": [ - { - "tagName": "download", - "attributeValues": { - "name": "go_default_sdk", - "version": "1.21.8" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", - "line": 17, - "column": 16 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@gazelle//:extensions.bzl", - "extensionName": "go_deps", - "usingModule": "rules_go@0.47.0", - "location": { - "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", - "line": 32, - "column": 24 - }, - "imports": { - "com_github_gogo_protobuf": "com_github_gogo_protobuf", - "com_github_golang_mock": "com_github_golang_mock", - "com_github_golang_protobuf": "com_github_golang_protobuf", - "org_golang_google_genproto": "org_golang_google_genproto", - "org_golang_google_grpc": "org_golang_google_grpc", - "org_golang_google_grpc_cmd_protoc_gen_go_grpc": "org_golang_google_grpc_cmd_protoc_gen_go_grpc", - "org_golang_google_protobuf": "org_golang_google_protobuf", - "org_golang_x_net": "org_golang_x_net", - "org_golang_x_tools": "org_golang_x_tools", - "bazel_gazelle_go_repository_config": "bazel_gazelle_go_repository_config" - }, - "devImports": [], - "tags": [ - { - "tagName": "from_file", - "attributeValues": { - "go_mod": "//:go.mod" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", - "line": 33, - "column": 18 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "io_bazel_rules_go_bazel_features": "bazel_features@1.9.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.9", - "rules_proto": "rules_proto@5.3.0-21.7", - "com_google_protobuf": "protobuf@21.7", - "gazelle": "gazelle@0.37.0", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_go/releases/download/v0.47.0/rules_go-v0.47.0.zip" - ], - "integrity": "sha256-r0fzDpy9cK405Jhm4gGz93Bpq7ERGD8sApfn50umu8A=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "rules_cc@0.0.9": { - "name": "rules_cc", - "version": "0.0.9", - "key": "rules_cc@0.0.9", - "repoName": "rules_cc", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@local_config_cc_toolchains//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@bazel_tools//tools/cpp:cc_configure.bzl", - "extensionName": "cc_configure_extension", - "usingModule": "rules_cc@0.0.9", - "location": { - "file": "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel", - "line": 9, - "column": 29 - }, - "imports": { - "local_config_cc_toolchains": "local_config_cc_toolchains" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_cc/releases/download/0.0.9/rules_cc-0.0.9.tar.gz" - ], - "integrity": "sha256-IDeHW5pEVtzkp50RKorohbvEqtlo5lh9ym5k86CQDN8=", - "strip_prefix": "rules_cc-0.0.9", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_cc/0.0.9/patches/module_dot_bazel_version.patch": "sha256-mM+qzOI0SgAdaJBlWOSMwMPKpaA9b7R37Hj/tp5bb4g=" - }, - "remote_patch_strip": 0 - } - } - }, - "platforms@0.0.9": { - "name": "platforms", - "version": "0.0.9", - "key": "platforms@0.0.9", - "repoName": "platforms", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@platforms//host:extension.bzl", - "extensionName": "host_platform", - "usingModule": "platforms@0.0.9", - "location": { - "file": "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel", - "line": 9, - "column": 30 - }, - "imports": { - "host_platform": "host_platform" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "rules_license": "rules_license@0.0.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/platforms/releases/download/0.0.9/platforms-0.0.9.tar.gz" - ], - "integrity": "sha256-XtpTnIQSZQMcL4LYrno6ZJC9YhduDAOPxGnqv5H2FJs=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "gazelle@0.37.0": { - "name": "gazelle", - "version": "0.37.0", - "key": "gazelle@0.37.0", - "repoName": "bazel_gazelle", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@io_bazel_rules_go//go:extensions.bzl", - "extensionName": "go_sdk", - "usingModule": "gazelle@0.37.0", - "location": { - "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", - "line": 13, - "column": 23 - }, - "imports": { - "go_host_compatible_sdk_label": "go_host_compatible_sdk_label" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_gazelle//internal/bzlmod:non_module_deps.bzl", - "extensionName": "non_module_deps", - "usingModule": "gazelle@0.37.0", - "location": { - "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", - "line": 21, - "column": 32 - }, - "imports": { - "bazel_gazelle_go_repository_cache": "bazel_gazelle_go_repository_cache", - "bazel_gazelle_go_repository_tools": "bazel_gazelle_go_repository_tools", - "bazel_gazelle_is_bazel_module": "bazel_gazelle_is_bazel_module" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", - "extensionName": "go_deps", - "usingModule": "gazelle@0.37.0", - "location": { - "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", - "line": 29, - "column": 24 - }, - "imports": { - "com_github_bazelbuild_buildtools": "com_github_bazelbuild_buildtools", - "com_github_bmatcuk_doublestar_v4": "com_github_bmatcuk_doublestar_v4", - "com_github_fsnotify_fsnotify": "com_github_fsnotify_fsnotify", - "com_github_google_go_cmp": "com_github_google_go_cmp", - "com_github_pmezard_go_difflib": "com_github_pmezard_go_difflib", - "org_golang_x_mod": "org_golang_x_mod", - "org_golang_x_sync": "org_golang_x_sync", - "org_golang_x_tools": "org_golang_x_tools", - "org_golang_x_tools_go_vcs": "org_golang_x_tools_go_vcs", - "bazel_gazelle_go_repository_config": "bazel_gazelle_go_repository_config", - "com_github_golang_protobuf": "com_github_golang_protobuf", - "org_golang_google_protobuf": "org_golang_google_protobuf" - }, - "devImports": [], - "tags": [ - { - "tagName": "from_file", - "attributeValues": { - "go_mod": "//:go.mod" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", - "line": 30, - "column": 18 - } - }, - { - "tagName": "module", - "attributeValues": { - "path": "golang.org/x/tools", - "sum": "h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=", - "version": "v0.18.0" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", - "line": 34, - "column": 15 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_features": "bazel_features@1.9.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "com_google_protobuf": "protobuf@21.7", - "io_bazel_rules_go": "rules_go@0.47.0", - "rules_proto": "rules_proto@5.3.0-21.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.37.0/bazel-gazelle-v0.37.0.tar.gz" - ], - "integrity": "sha256-12v3pg/YsFBEQJDfooN6Tq+YKeEWVhjuNdzspcvfWNU=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "bazel_features@1.9.1": { - "name": "bazel_features", - "version": "1.9.1", - "key": "bazel_features@1.9.1", - "repoName": "bazel_features", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@bazel_features//private:extensions.bzl", - "extensionName": "version_extension", - "usingModule": "bazel_features@1.9.1", - "location": { - "file": "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel", - "line": 15, - "column": 24 - }, - "imports": { - "bazel_features_globals": "bazel_features_globals", - "bazel_features_version": "bazel_features_version" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazel-contrib/bazel_features/releases/download/v1.9.1/bazel_features-v1.9.1.tar.gz" - ], - "integrity": "sha256-13h9oomn+0lzUiEa0gDsn2mIIqngdXpJdv2fcT/zcrM=", - "strip_prefix": "bazel_features-1.9.1", - "remote_patches": { - "https://bcr.bazel.build/modules/bazel_features/1.9.1/patches/module_dot_bazel_version.patch": "sha256-a2ofwS5r2Qq+WxzVa7sLbRXhfT3JoYxSlUVQH/nL454=" - }, - "remote_patch_strip": 1 - } - } - }, - "protobuf@21.7": { - "name": "protobuf", - "version": "21.7", - "key": "protobuf@21.7", - "repoName": "protobuf", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", - "extensionName": "maven", - "usingModule": "protobuf@21.7", - "location": { - "file": "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel", - "line": 22, - "column": 22 - }, - "imports": { - "maven": "maven" - }, - "devImports": [], - "tags": [ - { - "tagName": "install", - "attributeValues": { - "name": "maven", - "artifacts": [ - "com.google.code.findbugs:jsr305:3.0.2", - "com.google.code.gson:gson:2.8.9", - "com.google.errorprone:error_prone_annotations:2.3.2", - "com.google.j2objc:j2objc-annotations:1.3", - "com.google.guava:guava:31.1-jre", - "com.google.guava:guava-testlib:31.1-jre", - "com.google.truth:truth:1.1.2", - "junit:junit:4.13.2", - "org.mockito:mockito-core:4.3.1" - ] - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel", - "line": 24, - "column": 14 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_python": "rules_python@0.27.1", - "rules_cc": "rules_cc@0.0.9", - "rules_proto": "rules_proto@5.3.0-21.7", - "rules_java": "rules_java@7.4.0", - "rules_pkg": "rules_pkg@0.7.0", - "com_google_abseil": "abseil-cpp@20211102.0", - "zlib": "zlib@1.3", - "upb": "upb@0.0.0-20220923-a547704", - "rules_jvm_external": "rules_jvm_external@5.2", - "com_google_googletest": "googletest@1.11.0", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protobuf-all-21.7.zip" - ], - "integrity": "sha256-VJOiH17T/FAuZv7GuUScBqVRztYwAvpIkDxA36jeeko=", - "strip_prefix": "protobuf-21.7", - "remote_patches": { - "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_module_dot_bazel.patch": "sha256-q3V2+eq0v2XF0z8z+V+QF4cynD6JvHI1y3kI/+rzl5s=", - "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_module_dot_bazel_for_examples.patch": "sha256-O7YP6s3lo/1opUiO0jqXYORNHdZ/2q3hjz1QGy8QdIU=", - "https://bcr.bazel.build/modules/protobuf/21.7/patches/relative_repo_names.patch": "sha256-RK9RjW8T5UJNG7flIrnFiNE9vKwWB+8uWWtJqXYT0w4=", - "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_missing_files.patch": "sha256-Hyne4DG2u5bXcWHNxNMirA2QFAe/2Cl8oMm1XJdkQIY=" - }, - "remote_patch_strip": 1 - } - } - }, - "rules_proto@5.3.0-21.7": { - "name": "rules_proto", - "version": "5.3.0-21.7", - "key": "rules_proto@5.3.0-21.7", - "repoName": "rules_proto", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "com_google_protobuf": "protobuf@21.7", - "rules_cc": "rules_cc@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_proto/archive/refs/tags/5.3.0-21.7.tar.gz" - ], - "integrity": "sha256-3D+yBqLLNEG0heseQjFlsjEjWh6psDG0Qzz3vB+kYN0=", - "strip_prefix": "rules_proto-5.3.0-21.7", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "nlohmann_json@3.6.1": { - "name": "nlohmann_json", - "version": "3.6.1", - "key": "nlohmann_json@3.6.1", - "repoName": "nlohmann_json", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/nlohmann/json/releases/download/v3.6.1/include.zip" - ], - "integrity": "sha256-acyIIHzpE0fqUwsif/B3bbgty43mcE4aPXT0hBvGUc8=", - "strip_prefix": "", - "remote_patches": { - "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/patches/add_build_file.patch": "sha256-q7pmw7dn3H7Le3BgkydhrvZG+1e75JisnM+PLaPjCI0=", - "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/patches/module_dot_bazel.patch": "sha256-91o2FwT6kCVE+BBM+L2rxXFYbVDeDSkKHabs50GBa5o=" - }, - "remote_patch_strip": 0 - } - } - }, - "rules_python@0.27.1": { - "name": "rules_python", - "version": "0.27.1", - "key": "rules_python@0.27.1", - "repoName": "rules_python", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@pythons_hub//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_python//python/private/bzlmod:internal_deps.bzl", - "extensionName": "internal_deps", - "usingModule": "rules_python@0.27.1", - "location": { - "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", - "line": 17, - "column": 30 - }, - "imports": { - "rules_python_internal": "rules_python_internal", - "pypi__build": "pypi__build", - "pypi__click": "pypi__click", - "pypi__colorama": "pypi__colorama", - "pypi__importlib_metadata": "pypi__importlib_metadata", - "pypi__installer": "pypi__installer", - "pypi__more_itertools": "pypi__more_itertools", - "pypi__packaging": "pypi__packaging", - "pypi__pep517": "pypi__pep517", - "pypi__pip": "pypi__pip", - "pypi__pip_tools": "pypi__pip_tools", - "pypi__pyproject_hooks": "pypi__pyproject_hooks", - "pypi__setuptools": "pypi__setuptools", - "pypi__tomli": "pypi__tomli", - "pypi__wheel": "pypi__wheel", - "pypi__zipp": "pypi__zipp" - }, - "devImports": [], - "tags": [ - { - "tagName": "install", - "attributeValues": {}, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", - "line": 18, - "column": 22 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@rules_python//python/extensions:python.bzl", - "extensionName": "python", - "usingModule": "rules_python@0.27.1", - "location": { - "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", - "line": 43, - "column": 23 - }, - "imports": { - "pythons_hub": "pythons_hub" - }, - "devImports": [], - "tags": [ - { - "tagName": "toolchain", - "attributeValues": { - "is_default": true, - "python_version": "3.11" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", - "line": 49, - "column": 17 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_features": "bazel_features@1.9.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.9", - "rules_proto": "rules_proto@5.3.0-21.7", - "com_google_protobuf": "protobuf@21.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_python/releases/download/0.27.1/rules_python-0.27.1.tar.gz" - ], - "integrity": "sha256-6FrjDeM2JaY+yn/ECpT+qEXmQYiOUvMra+6pHosbJ5M=", - "strip_prefix": "rules_python-0.27.1", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_python/0.27.1/patches/module_dot_bazel_version.patch": "sha256-Ier7Gb4zhbS273tClCov24gNYdheo4FdegZwaHBrQy0=" - }, - "remote_patch_strip": 1 - } - } - }, - "rules_java@7.4.0": { - "name": "rules_java", - "version": "7.4.0", - "key": "rules_java@7.4.0", - "repoName": "rules_java", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "//toolchains:all", - "@local_jdk//:runtime_toolchain_definition", - "@local_jdk//:bootstrap_runtime_toolchain_definition", - "@remotejdk11_linux_toolchain_config_repo//:all", - "@remotejdk11_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk11_linux_ppc64le_toolchain_config_repo//:all", - "@remotejdk11_linux_s390x_toolchain_config_repo//:all", - "@remotejdk11_macos_toolchain_config_repo//:all", - "@remotejdk11_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk11_win_toolchain_config_repo//:all", - "@remotejdk11_win_arm64_toolchain_config_repo//:all", - "@remotejdk17_linux_toolchain_config_repo//:all", - "@remotejdk17_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk17_linux_ppc64le_toolchain_config_repo//:all", - "@remotejdk17_linux_s390x_toolchain_config_repo//:all", - "@remotejdk17_macos_toolchain_config_repo//:all", - "@remotejdk17_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk17_win_toolchain_config_repo//:all", - "@remotejdk17_win_arm64_toolchain_config_repo//:all", - "@remotejdk21_linux_toolchain_config_repo//:all", - "@remotejdk21_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk21_macos_toolchain_config_repo//:all", - "@remotejdk21_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk21_win_toolchain_config_repo//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_java//java:extensions.bzl", - "extensionName": "toolchains", - "usingModule": "rules_java@7.4.0", - "location": { - "file": "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel", - "line": 19, - "column": 27 - }, - "imports": { - "remote_java_tools": "remote_java_tools", - "remote_java_tools_linux": "remote_java_tools_linux", - "remote_java_tools_windows": "remote_java_tools_windows", - "remote_java_tools_darwin_x86_64": "remote_java_tools_darwin_x86_64", - "remote_java_tools_darwin_arm64": "remote_java_tools_darwin_arm64", - "local_jdk": "local_jdk", - "remotejdk11_linux_toolchain_config_repo": "remotejdk11_linux_toolchain_config_repo", - "remotejdk11_linux_aarch64_toolchain_config_repo": "remotejdk11_linux_aarch64_toolchain_config_repo", - "remotejdk11_linux_ppc64le_toolchain_config_repo": "remotejdk11_linux_ppc64le_toolchain_config_repo", - "remotejdk11_linux_s390x_toolchain_config_repo": "remotejdk11_linux_s390x_toolchain_config_repo", - "remotejdk11_macos_toolchain_config_repo": "remotejdk11_macos_toolchain_config_repo", - "remotejdk11_macos_aarch64_toolchain_config_repo": "remotejdk11_macos_aarch64_toolchain_config_repo", - "remotejdk11_win_toolchain_config_repo": "remotejdk11_win_toolchain_config_repo", - "remotejdk11_win_arm64_toolchain_config_repo": "remotejdk11_win_arm64_toolchain_config_repo", - "remotejdk17_linux_toolchain_config_repo": "remotejdk17_linux_toolchain_config_repo", - "remotejdk17_linux_aarch64_toolchain_config_repo": "remotejdk17_linux_aarch64_toolchain_config_repo", - "remotejdk17_linux_ppc64le_toolchain_config_repo": "remotejdk17_linux_ppc64le_toolchain_config_repo", - "remotejdk17_linux_s390x_toolchain_config_repo": "remotejdk17_linux_s390x_toolchain_config_repo", - "remotejdk17_macos_toolchain_config_repo": "remotejdk17_macos_toolchain_config_repo", - "remotejdk17_macos_aarch64_toolchain_config_repo": "remotejdk17_macos_aarch64_toolchain_config_repo", - "remotejdk17_win_toolchain_config_repo": "remotejdk17_win_toolchain_config_repo", - "remotejdk17_win_arm64_toolchain_config_repo": "remotejdk17_win_arm64_toolchain_config_repo", - "remotejdk21_linux_toolchain_config_repo": "remotejdk21_linux_toolchain_config_repo", - "remotejdk21_linux_aarch64_toolchain_config_repo": "remotejdk21_linux_aarch64_toolchain_config_repo", - "remotejdk21_macos_toolchain_config_repo": "remotejdk21_macos_toolchain_config_repo", - "remotejdk21_macos_aarch64_toolchain_config_repo": "remotejdk21_macos_aarch64_toolchain_config_repo", - "remotejdk21_win_toolchain_config_repo": "remotejdk21_win_toolchain_config_repo" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "platforms": "platforms@0.0.9", - "rules_cc": "rules_cc@0.0.9", - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_proto": "rules_proto@5.3.0-21.7", - "rules_license": "rules_license@0.0.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_java/releases/download/7.4.0/rules_java-7.4.0.tar.gz" - ], - "integrity": "sha256-l27wi0nJKXQfIBeQ5Z44B8cq2B9CjIvJU82+/1/tFes=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "rules_license@0.0.7": { - "name": "rules_license", - "version": "0.0.7", - "key": "rules_license@0.0.7", - "repoName": "rules_license", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz" - ], - "integrity": "sha256-RTHezLkTY5ww5cdRKgVNXYdWmNrrddjPkPKEN1/nw2A=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "buildozer@6.4.0.2": { - "name": "buildozer", - "version": "6.4.0.2", - "key": "buildozer@6.4.0.2", - "repoName": "buildozer", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@buildozer//:buildozer_binary.bzl", - "extensionName": "buildozer_binary", - "usingModule": "buildozer@6.4.0.2", - "location": { - "file": "https://bcr.bazel.build/modules/buildozer/6.4.0.2/MODULE.bazel", - "line": 7, - "column": 33 - }, - "imports": { - "buildozer_binary": "buildozer_binary" - }, - "devImports": [], - "tags": [ - { - "tagName": "buildozer", - "attributeValues": { - "sha256": { - "darwin-amd64": "d29e347ecd6b5673d72cb1a8de05bf1b06178dd229ff5eb67fad5100c840cc8e", - "darwin-arm64": "9b9e71bdbec5e7223871e913b65d12f6d8fa026684daf991f00e52ed36a6978d", - "linux-amd64": "8dfd6345da4e9042daa738d7fdf34f699c5dfce4632f7207956fceedd8494119", - "linux-arm64": "6559558fded658c8fa7432a9d011f7c4dcbac6b738feae73d2d5c352e5f605fa", - "windows-amd64": "e7f05bf847f7c3689dd28926460ce6e1097ae97380ac8e6ae7147b7b706ba19b" - }, - "version": "6.4.0" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/buildozer/6.4.0.2/MODULE.bazel", - "line": 8, - "column": 27 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/fmeum/buildozer/releases/download/v6.4.0.2/buildozer-v6.4.0.2.tar.gz" - ], - "integrity": "sha256-k7tFKQMR2AygxpmZfH0yEPnQmF3efFgD9rBPkj+Yz/8=", - "strip_prefix": "buildozer-6.4.0.2", - "remote_patches": { - "https://bcr.bazel.build/modules/buildozer/6.4.0.2/patches/module_dot_bazel_version.patch": "sha256-gKANF2HMilj7bWmuXs4lbBIAAansuWC4IhWGB/CerjU=" - }, - "remote_patch_strip": 1 - } - } - }, - "zlib@1.3": { - "name": "zlib", - "version": "1.3", - "key": "zlib@1.3", - "repoName": "zlib", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "platforms": "platforms@0.0.9", - "rules_cc": "rules_cc@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/madler/zlib/releases/download/v1.3/zlib-1.3.tar.gz" - ], - "integrity": "sha256-/wukwpIBPbwnUws6geH5qBPNOd4Byl4Pi/NVcC76WT4=", - "strip_prefix": "zlib-1.3", - "remote_patches": { - "https://bcr.bazel.build/modules/zlib/1.3/patches/add_build_file.patch": "sha256-Ei+FYaaOo7A3jTKunMEodTI0Uw5NXQyZEcboMC8JskY=", - "https://bcr.bazel.build/modules/zlib/1.3/patches/module_dot_bazel.patch": "sha256-fPWLM+2xaF/kuy+kZc1YTfW6hNjrkG400Ho7gckuyJk=" - }, - "remote_patch_strip": 0 - } - } - }, - "stardoc@0.6.2": { - "name": "stardoc", - "version": "0.6.2", - "key": "stardoc@0.6.2", - "repoName": "stardoc", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", - "extensionName": "maven", - "usingModule": "stardoc@0.6.2", - "location": { - "file": "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel", - "line": 22, - "column": 22 - }, - "imports": { - "stardoc_maven": "stardoc_maven" - }, - "devImports": [], - "tags": [ - { - "tagName": "install", - "attributeValues": { - "name": "stardoc_maven", - "artifacts": [ - "com.beust:jcommander:1.82", - "com.google.escapevelocity:escapevelocity:1.1", - "com.google.guava:guava:31.1-jre", - "com.google.truth:truth:1.1.3", - "junit:junit:4.13.2" - ], - "fail_if_repin_required": true, - "lock_file": "//:maven_install.json", - "repositories": [ - "https://repo1.maven.org/maven2" - ], - "strict_visibility": true - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel", - "line": 23, - "column": 14 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_java": "rules_java@7.4.0", - "rules_jvm_external": "rules_jvm_external@5.2", - "rules_license": "rules_license@0.0.7", - "com_google_protobuf": "protobuf@21.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/stardoc/releases/download/0.6.2/stardoc-0.6.2.tar.gz" - ], - "integrity": "sha256-Yr0uYCFrem/sOseTQaogHglWR358j2zMKG8nmtHZZDI=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "buildifier_prebuilt@6.0.0.1": { - "name": "buildifier_prebuilt", - "version": "6.0.0.1", - "key": "buildifier_prebuilt@6.0.0.1", - "repoName": "buildifier_prebuilt", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@buildifier_prebuilt_toolchains//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@buildifier_prebuilt//:defs.bzl", - "extensionName": "buildifier_prebuilt_deps_extension", - "usingModule": "buildifier_prebuilt@6.0.0.1", - "location": { - "file": "https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/MODULE.bazel", - "line": 10, - "column": 32 - }, - "imports": { - "buildifier_prebuilt_toolchains": "buildifier_prebuilt_toolchains" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/keith/buildifier-prebuilt/archive/refs/tags/6.0.0.1.tar.gz" - ], - "integrity": "sha256-9wk6lgqMNHFVJ2SJLOEsti2bcmAPpMiwiyCQxF2wXOg=", - "strip_prefix": "buildifier-prebuilt-6.0.0.1", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "rules_pkg@0.7.0": { - "name": "rules_pkg", - "version": "0.7.0", - "key": "rules_pkg@0.7.0", - "repoName": "rules_pkg", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "rules_python": "rules_python@0.27.1", - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_license": "rules_license@0.0.7", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_pkg/releases/download/0.7.0/rules_pkg-0.7.0.tar.gz" - ], - "integrity": "sha256-iimOgydi7aGDBZfWT+fbWBeKqEzVkm121bdE1lWJQcI=", - "strip_prefix": "", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_pkg/0.7.0/patches/module_dot_bazel.patch": "sha256-4OaEPZwYF6iC71ZTDg6MJ7LLqX7ZA0/kK4mT+4xKqiE=" - }, - "remote_patch_strip": 0 - } - } - }, - "abseil-cpp@20211102.0": { - "name": "abseil-cpp", - "version": "20211102.0", - "key": "abseil-cpp@20211102.0", - "repoName": "abseil-cpp", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "rules_cc": "rules_cc@0.0.9", - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/abseil/abseil-cpp/archive/refs/tags/20211102.0.tar.gz" - ], - "integrity": "sha256-3PcbnLqNwMqZQMSzFqDHlr6Pq0KwcLtrfKtitI8OZsQ=", - "strip_prefix": "abseil-cpp-20211102.0", - "remote_patches": { - "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/patches/module_dot_bazel.patch": "sha256-4izqopgGCey4jVZzl/w3M2GVPNohjh2B5TmbThZNvPY=" - }, - "remote_patch_strip": 0 - } - } - }, - "upb@0.0.0-20220923-a547704": { - "name": "upb", - "version": "0.0.0-20220923-a547704", - "key": "upb@0.0.0-20220923-a547704", - "repoName": "upb", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_proto": "rules_proto@5.3.0-21.7", - "com_google_protobuf": "protobuf@21.7", - "com_google_absl": "abseil-cpp@20211102.0", - "platforms": "platforms@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/protocolbuffers/upb/archive/a5477045acaa34586420942098f5fecd3570f577.tar.gz" - ], - "integrity": "sha256-z39x6v+QskwaKLSWRan/A6mmwecTQpHOcJActj5zZLU=", - "strip_prefix": "upb-a5477045acaa34586420942098f5fecd3570f577", - "remote_patches": { - "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/patches/module_dot_bazel.patch": "sha256-wH4mNS6ZYy+8uC0HoAft/c7SDsq2Kxf+J8dUakXhaB0=" - }, - "remote_patch_strip": 0 - } - } - }, - "rules_jvm_external@5.2": { - "name": "rules_jvm_external", - "version": "5.2", - "key": "rules_jvm_external@5.2", - "repoName": "rules_jvm_external", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_jvm_external//:non-module-deps.bzl", - "extensionName": "non_module_deps", - "usingModule": "rules_jvm_external@5.2", - "location": { - "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", - "line": 9, - "column": 32 - }, - "imports": { - "io_bazel_rules_kotlin": "io_bazel_rules_kotlin" - }, - "devImports": [], - "tags": [], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - }, - { - "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", - "extensionName": "maven", - "usingModule": "rules_jvm_external@5.2", - "location": { - "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", - "line": 15, - "column": 22 - }, - "imports": { - "rules_jvm_external_deps": "rules_jvm_external_deps" - }, - "devImports": [], - "tags": [ - { - "tagName": "install", - "attributeValues": { - "name": "rules_jvm_external_deps", - "artifacts": [ - "com.google.auth:google-auth-library-credentials:0.22.0", - "com.google.auth:google-auth-library-oauth2-http:0.22.0", - "com.google.cloud:google-cloud-core:1.93.10", - "com.google.cloud:google-cloud-storage:1.113.4", - "com.google.code.gson:gson:2.9.0", - "com.google.googlejavaformat:google-java-format:1.15.0", - "com.google.guava:guava:31.1-jre", - "org.apache.maven:maven-artifact:3.8.6", - "software.amazon.awssdk:s3:2.17.183" - ], - "lock_file": "@rules_jvm_external//:rules_jvm_external_deps_install.json" - }, - "devDependency": false, - "location": { - "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", - "line": 16, - "column": 14 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], - "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "io_bazel_stardoc": "stardoc@0.6.2", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.2/rules_jvm_external-5.2.tar.gz" - ], - "integrity": "sha256-+G/UKoCeGHHKCqvonbDUQEUSGcPORsWNokDH3NwAEl8=", - "strip_prefix": "rules_jvm_external-5.2", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, - "googletest@1.11.0": { - "name": "googletest", - "version": "1.11.0", - "key": "googletest@1.11.0", - "repoName": "googletest", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "com_google_absl": "abseil-cpp@20211102.0", - "platforms": "platforms@0.0.9", - "rules_cc": "rules_cc@0.0.9", - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz" - ], - "integrity": "sha256-tIcL8SH/d5W6INILzdhie44Ijy0dqymaAxwQNO3ck9U=", - "strip_prefix": "googletest-release-1.11.0", - "remote_patches": { - "https://bcr.bazel.build/modules/googletest/1.11.0/patches/module_dot_bazel.patch": "sha256-HuahEdI/n8KCI071sN3CEziX+7qP/Ec77IWayYunLP0=" - }, - "remote_patch_strip": 0 - } - } - } + "lockFileVersion": 11, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad", + "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", + "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", + "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015", + "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/source.json": "082ed5f9837901fada8c68c2f3ddc958bb22b6d654f71dd73f3df30d45d4b749", + "https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/MODULE.bazel": "5d23708e6a5527ab4f151da7accabc22808cb5fb579c8cc4cd4a292da57a5c97", + "https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/source.json": "57bc8b05bd4bb2736efe1b41f9f4bf551cdced8314e8d20420b8a0e5a0751b30", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/MODULE.bazel": "6184e95b48699a8864d0ece97efe544488ad2e8dc5abc6e57561466d921c68d7", + "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/source.json": "e5bd0650c438cff88fd8ac5db9694e0b81c8f904dd5295c7060da4cd4a1412d3", + "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://bcr.bazel.build/modules/gazelle/0.38.0/MODULE.bazel": "51bb3ca009bc9320492894aece6ba5f50aae68a39fff2567844b77fc12e2d0a5", + "https://bcr.bazel.build/modules/gazelle/0.38.0/source.json": "7fedf9b531bcbbe90b009e4d3aef478a2defb8b8a6e31e931445231e425fc37c", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", + "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel": "e425890d2a4d668abc0f59d8388b70bf63ad025edec76a385c35d85882519417", + "https://bcr.bazel.build/modules/rules_go/0.47.0/source.json": "24525061cea6a0829d989b00d90f1da6b225f4c39cdd512f8e406616cb29e044", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", + "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", + "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/source.json": "10572111995bc349ce31c78f74b3c147f6b3233975c7fa5eff9211f6db0d34d9", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", + "https://bcr.bazel.build/modules/rules_license/0.0.8/source.json": "ccfd3964cd0cd1739202efb8dbf9a06baab490e61e174b2ad4790f9c4e610beb", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", + "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", + "https://bcr.bazel.build/modules/rules_python/0.27.1/source.json": "88980dfdcf0651a11344cad2ab1f962ea1b0e51edc80ebbae274c8fa9cde78f4", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_swift_package_manager/0.39.0/MODULE.bazel": "c6417e71c2195bc19790b504da585c881f18698a1dd2143f445ee6f9468cbe79", + "https://bcr.bazel.build/modules/rules_swift_package_manager/0.39.0/source.json": "f058ff850a1285b15030a2d400f13972ee9a104ff8b62614d28ae76eb0b16dc3", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", + "https://bcr.bazel.build/modules/stardoc/0.6.2/source.json": "d2ff8063b63b4a85e65fe595c4290f99717434fa9f95b4748a79a7d04dfed349", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d" }, + "selectedYankedVersions": {}, "moduleExtensions": { "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "RyR+EbN4fAzxxZSQKwXXrxEtMVrezn79MOR/2mmcmYk=", + "bzlTransitiveDigest": "7ii+gFxWSxHhQPrBxfMEHhtrGvHmBTvsh+KOyGunP/s=", + "usagesDigest": "/PMkj2kozjlzGpn8KnLGKH3e78EYb/33JEPkOxmXM/0=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -1867,104 +123,10 @@ ] } }, - "@@bazel_features~//private:extensions.bzl%version_extension": { - "general": { - "bzlTransitiveDigest": "3FcE0iMy2yYKEbEO19f72k9dzcpRUXHH+igow5yVy8g=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "bazel_features_version": { - "bzlFile": "@@bazel_features~//private:version_repo.bzl", - "ruleClassName": "version_repo", - "attributes": {} - }, - "bazel_features_globals": { - "bzlFile": "@@bazel_features~//private:globals_repo.bzl", - "ruleClassName": "globals_repo", - "attributes": { - "globals": { - "RunEnvironmentInfo": "5.3.0", - "DefaultInfo": "0.0.1", - "__TestingOnly_NeverAvailable": "1000000000.0.0" - } - } - } - }, - "recordedRepoMappingEntries": [ - [ - "bazel_features~", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, - "@@bazel_tools//tools/cpp:cc_configure.bzl%cc_configure_extension": { - "general": { - "bzlTransitiveDigest": "PHpT2yqMGms2U4L3E/aZ+WcQalmZWm+ILdP3yiLsDhA=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "local_config_cc": { - "bzlFile": "@@bazel_tools//tools/cpp:cc_configure.bzl", - "ruleClassName": "cc_autoconf", - "attributes": {} - }, - "local_config_cc_toolchains": { - "bzlFile": "@@bazel_tools//tools/cpp:cc_configure.bzl", - "ruleClassName": "cc_autoconf_toolchains", - "attributes": {} - } - }, - "recordedRepoMappingEntries": [ - [ - "bazel_tools", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, - "@@bazel_tools//tools/osx:xcode_configure.bzl%xcode_configure_extension": { - "general": { - "bzlTransitiveDigest": "Qh2bWTU6QW6wkrd87qrU4YeY+SG37Nvw3A0PR4Y0L2Y=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "local_config_xcode": { - "bzlFile": "@@bazel_tools//tools/osx:xcode_configure.bzl", - "ruleClassName": "xcode_autoconf", - "attributes": { - "xcode_locator": "@bazel_tools//tools/osx:xcode_locator.m", - "remote_xcode": "" - } - } - }, - "recordedRepoMappingEntries": [] - } - }, - "@@bazel_tools//tools/sh:sh_configure.bzl%sh_configure_extension": { - "general": { - "bzlTransitiveDigest": "hp4NgmNjEg5+xgvzfh6L83bt9/aiiWETuNpwNuF1MSU=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "local_config_sh": { - "bzlFile": "@@bazel_tools//tools/sh:sh_configure.bzl", - "ruleClassName": "sh_config", - "attributes": {} - } - }, - "recordedRepoMappingEntries": [] - } - }, "@@buildifier_prebuilt~//:defs.bzl%buildifier_prebuilt_deps_extension": { "general": { - "bzlTransitiveDigest": "ZLz8nU7kwZ0wfwY66sYRmuSjBB3R2KenLwVQfaHkvRI=", + "bzlTransitiveDigest": "OoWi5m9k5b1vcdkzlL5LgptXPW6UHcvdioFl1jYehOU=", + "usagesDigest": "XfKr5E71RWCdeHzfpN7UfG8Uh5FTpmmLv0mGGeiymYM=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -2087,539 +249,27 @@ ] } }, - "@@buildozer~//:buildozer_binary.bzl%buildozer_binary": { + "@@platforms//host:extension.bzl%host_platform": { "general": { - "bzlTransitiveDigest": "EleDU/FQ1+e/RgkW3aIDmdaxZEthvoWQhsqFTxiSgMI=", + "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=", + "usagesDigest": "meSzxn3DUCcYEhq4HQwExWkWtU4EjriRBQLsZN+Q0SU=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "buildozer_binary": { - "bzlFile": "@@buildozer~//private:buildozer_binary.bzl", - "ruleClassName": "_buildozer_binary_repo", - "attributes": { - "sha256": { - "darwin-amd64": "d29e347ecd6b5673d72cb1a8de05bf1b06178dd229ff5eb67fad5100c840cc8e", - "darwin-arm64": "9b9e71bdbec5e7223871e913b65d12f6d8fa026684daf991f00e52ed36a6978d", - "linux-amd64": "8dfd6345da4e9042daa738d7fdf34f699c5dfce4632f7207956fceedd8494119", - "linux-arm64": "6559558fded658c8fa7432a9d011f7c4dcbac6b738feae73d2d5c352e5f605fa", - "windows-amd64": "e7f05bf847f7c3689dd28926460ce6e1097ae97380ac8e6ae7147b7b706ba19b" - }, - "version": "6.4.0" - } + "host_platform": { + "bzlFile": "@@platforms//host:extension.bzl", + "ruleClassName": "host_platform_repo", + "attributes": {} } }, "recordedRepoMappingEntries": [] } }, - "@@rules_java~//java:extensions.bzl%toolchains": { - "general": { - "bzlTransitiveDigest": "tJHbmWnq7m+9eUBnUdv7jZziQ26FmcGL9C5/hU3Q9UQ=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "remotejdk21_linux_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux//:jdk\",\n)\n" - } - }, - "remotejdk17_linux_s390x_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_s390x//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_s390x//:jdk\",\n)\n" - } - }, - "remotejdk17_macos_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos//:jdk\",\n)\n" - } - }, - "remotejdk21_macos_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk17_linux_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk21_macos_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", - "sha256": "e8260516de8b60661422a725f1df2c36ef888f6fb35393566b00e7325db3d04e", - "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-macosx_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_aarch64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_aarch64.tar.gz" - ] - } - }, - "remotejdk17_linux_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux//:jdk\",\n)\n" - } - }, - "remotejdk17_macos_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "314b04568ec0ae9b36ba03c9cbd42adc9e1265f74678923b19297d66eb84dcca", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64.tar.gz" - ] - } - }, - "remote_java_tools_windows": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "sha256": "fe2f88169696d6c6fc6e90ba61bb46be7d0ae3693cbafdf336041bf56679e8d1", - "urls": [ - "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_windows-v13.4.zip", - "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_windows-v13.4.zip" - ] - } - }, - "remotejdk11_win": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "43408193ce2fa0862819495b5ae8541085b95660153f2adcf91a52d3a1710e83", - "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-win_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-win_x64.zip", - "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-win_x64.zip" - ] - } - }, - "remotejdk11_win_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win//:jdk\",\n)\n" - } - }, - "remotejdk11_linux_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "54174439f2b3fddd11f1048c397fe7bb45d4c9d66d452d6889b013d04d21c4de", - "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-linux_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_aarch64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_aarch64.tar.gz" - ] - } - }, - "remotejdk17_linux": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "b9482f2304a1a68a614dfacddcf29569a72f0fac32e6c74f83dc1b9a157b8340", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-linux_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_x64.tar.gz" - ] - } - }, - "remotejdk11_linux_s390x_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_s390x//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_s390x//:jdk\",\n)\n" - } - }, - "remotejdk11_linux_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux//:jdk\",\n)\n" - } - }, - "remotejdk11_macos": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "bcaab11cfe586fae7583c6d9d311c64384354fb2638eb9a012eca4c3f1a1d9fd", - "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-macosx_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_x64.tar.gz" - ] - } - }, - "remotejdk11_win_arm64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "b8a28e6e767d90acf793ea6f5bed0bb595ba0ba5ebdf8b99f395266161e53ec2", - "strip_prefix": "jdk-11.0.13+8", - "urls": [ - "https://mirror.bazel.build/aka.ms/download-jdk/microsoft-jdk-11.0.13.8.1-windows-aarch64.zip" - ] - } - }, - "remotejdk17_macos": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "640453e8afe8ffe0fb4dceb4535fb50db9c283c64665eebb0ba68b19e65f4b1f", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-macosx_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_x64.tar.gz" - ] - } - }, - "remotejdk21_macos": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", - "sha256": "3ad8fe288eb57d975c2786ae453a036aa46e47ab2ac3d81538ebae2a54d3c025", - "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-macosx_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_x64.tar.gz" - ] - } - }, - "remotejdk21_macos_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos//:jdk\",\n)\n" - } - }, - "remotejdk17_macos_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk17_win": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "192f2afca57701de6ec496234f7e45d971bf623ff66b8ee4a5c81582054e5637", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-win_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_x64.zip", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_x64.zip" - ] - } - }, - "remotejdk11_macos_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk11_linux_ppc64le_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_ppc64le//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_ppc64le//:jdk\",\n)\n" - } - }, - "remotejdk21_linux": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", - "sha256": "5ad730fbee6bb49bfff10bf39e84392e728d89103d3474a7e5def0fd134b300a", - "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-linux_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_x64.tar.gz" - ] - } - }, - "remote_java_tools_linux": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "sha256": "ba10f09a138cf185d04cbc807d67a3da42ab13d618c5d1ce20d776e199c33a39", - "urls": [ - "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_linux-v13.4.zip", - "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_linux-v13.4.zip" - ] - } - }, - "remotejdk21_win": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", - "sha256": "f7cc15ca17295e69c907402dfe8db240db446e75d3b150da7bf67243cded93de", - "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-win_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-win_x64.zip", - "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-win_x64.zip" - ] - } - }, - "remotejdk21_linux_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", - "sha256": "ce7df1af5d44a9f455617c4b8891443fbe3e4b269c777d8b82ed66f77167cfe0", - "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-linux_aarch64", - "urls": [ - "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz", - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz" - ] - } - }, - "remotejdk11_linux_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk11_linux_s390x": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "a58fc0361966af0a5d5a31a2d8a208e3c9bb0f54f345596fd80b99ea9a39788b", - "strip_prefix": "jdk-11.0.15+10", - "urls": [ - "https://mirror.bazel.build/github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_s390x_linux_hotspot_11.0.15_10.tar.gz", - "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_s390x_linux_hotspot_11.0.15_10.tar.gz" - ] - } - }, - "remotejdk17_linux_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "6531cef61e416d5a7b691555c8cf2bdff689201b8a001ff45ab6740062b44313", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64.tar.gz" - ] - } - }, - "remotejdk17_win_arm64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win_arm64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win_arm64//:jdk\",\n)\n" - } - }, - "remotejdk11_linux": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "a34b404f87a08a61148b38e1416d837189e1df7a040d949e743633daf4695a3c", - "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-linux_x64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_x64.tar.gz" - ] - } - }, - "remotejdk11_macos_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos//:jdk\",\n)\n" - } - }, - "remotejdk17_linux_ppc64le_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_ppc64le//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_ppc64le//:jdk\",\n)\n" - } - }, - "remotejdk17_win_arm64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "6802c99eae0d788e21f52d03cab2e2b3bf42bc334ca03cbf19f71eb70ee19f85", - "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-win_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_aarch64.zip", - "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_aarch64.zip" - ] - } - }, - "remote_java_tools_darwin_arm64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "sha256": "076a7e198ad077f8c7d997986ef5102427fae6bbfce7a7852d2e080ed8767528", - "urls": [ - "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_darwin_arm64-v13.4.zip", - "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_darwin_arm64-v13.4.zip" - ] - } - }, - "remotejdk17_linux_ppc64le": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "00a4c07603d0218cd678461b5b3b7e25b3253102da4022d31fc35907f21a2efd", - "strip_prefix": "jdk-17.0.8.1+1", - "urls": [ - "https://mirror.bazel.build/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_ppc64le_linux_hotspot_17.0.8.1_1.tar.gz", - "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_ppc64le_linux_hotspot_17.0.8.1_1.tar.gz" - ] - } - }, - "remotejdk21_linux_aarch64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux_aarch64//:jdk\",\n)\n" - } - }, - "remotejdk11_win_arm64_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win_arm64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win_arm64//:jdk\",\n)\n" - } - }, - "local_jdk": { - "bzlFile": "@@rules_java~//toolchains:local_java_repository.bzl", - "ruleClassName": "_local_java_repository_rule", - "attributes": { - "java_home": "", - "version": "", - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = {RUNTIME_VERSION},\n)\n" - } - }, - "remote_java_tools_darwin_x86_64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "sha256": "4523aec4d09c587091a2dae6f5c9bc6922c220f3b6030e5aba9c8f015913cc65", - "urls": [ - "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_darwin_x86_64-v13.4.zip", - "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_darwin_x86_64-v13.4.zip" - ] - } - }, - "remote_java_tools": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "sha256": "e025fd260ac39b47c111f5212d64ec0d00d85dec16e49368aae82fc626a940cf", - "urls": [ - "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools-v13.4.zip", - "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools-v13.4.zip" - ] - } - }, - "remotejdk17_linux_s390x": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", - "sha256": "ffacba69c6843d7ca70d572489d6cc7ab7ae52c60f0852cedf4cf0d248b6fc37", - "strip_prefix": "jdk-17.0.8.1+1", - "urls": [ - "https://mirror.bazel.build/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_s390x_linux_hotspot_17.0.8.1_1.tar.gz", - "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_s390x_linux_hotspot_17.0.8.1_1.tar.gz" - ] - } - }, - "remotejdk17_win_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win//:jdk\",\n)\n" - } - }, - "remotejdk11_linux_ppc64le": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "a8fba686f6eb8ae1d1a9566821dbd5a85a1108b96ad857fdbac5c1e4649fc56f", - "strip_prefix": "jdk-11.0.15+10", - "urls": [ - "https://mirror.bazel.build/github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_ppc64le_linux_hotspot_11.0.15_10.tar.gz", - "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_ppc64le_linux_hotspot_11.0.15_10.tar.gz" - ] - } - }, - "remotejdk11_macos_aarch64": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", - "sha256": "7632bc29f8a4b7d492b93f3bc75a7b61630894db85d136456035ab2a24d38885", - "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-macosx_aarch64", - "urls": [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_aarch64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_aarch64.tar.gz" - ] - } - }, - "remotejdk21_win_toolchain_config_repo": { - "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", - "ruleClassName": "_toolchain_config", - "attributes": { - "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_win//:jdk\",\n)\n" - } - } - }, - "recordedRepoMappingEntries": [ - [ - "rules_java~", - "bazel_tools", - "bazel_tools" - ], - [ - "rules_java~", - "remote_java_tools", - "rules_java~~toolchains~remote_java_tools" - ] - ] - } - }, "@@rules_python~//python/extensions:python.bzl%python": { "general": { - "bzlTransitiveDigest": "7a7jv196EvOxUgMutfPERBnBTrOv3o1Z/xJPUFb/WXc=", + "bzlTransitiveDigest": "/yOJamf205SazTZcWeVppJIMp+ft3hLNKXSwoDADZNo=", + "usagesDigest": "ZE2I3XMaBI3a/ZeZaclrZU48gDCvEch5L5cq9Ek70e8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -2807,7 +457,8 @@ }, "@@rules_python~//python/private/bzlmod:internal_deps.bzl%internal_deps": { "general": { - "bzlTransitiveDigest": "eaQ4wDSCMX120Gyjf8GAwHl9I1TWl0raUq3qbaqafjg=", + "bzlTransitiveDigest": "BBwtJlGgWcMoGDUgN/9oMYPkMEig0gnViMArGuYOgvw=", + "usagesDigest": "0KtJR32xTfnnwt2hOSAdBgKoWuOMjlmHn+Po/xBs5Aw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -2979,10 +630,11 @@ }, "@@rules_swift_package_manager~//:extensions.bzl%swift_deps": { "general": { - "bzlTransitiveDigest": "YjE3dFjYQ4sj5gn2Iz1cWVK14/ZJ5cmnAUUGjDbACAA=", + "bzlTransitiveDigest": "8/YWyYftd8THfVoADvrOmQLl45wUGfP2MVjLM5FFn50=", + "usagesDigest": "voXBMcSNlo2fnK6JIvInIrncYhBKKG8nBeKvToaUA0Y=", "recordedFileInputs": { - "@@//Package.resolved": "4bebda61b63f729dd3d79fffe2890c072168135df0c4aeb5fbc1ee45b361d05e", - "@@//Package.swift": "fb3cb1d48066e64f8bf17fe1a49f689b7a6bf4bfc07aa90b9b80a02188501951" + "@@//Package.resolved": "585508e14a2051d4181c2d8f5481905287797bab9ab75b7f83482251f54b6e3c", + "@@//Package.swift": "b9e2b86f6f986c3f74ad4806e3d35d2fd2a8097270805eb903ccb36da8935aac" }, "recordedDirentsInputs": {}, "envVariables": {}, @@ -2994,6 +646,7 @@ "bazel_package_name": "swiftpkg_single_factor_auth_swift", "commit": "44e222ea3fcec4faf17c5813f832cfc1d2d06d4b", "remote": "https://github.com/Web3Auth/single-factor-auth-swift.git", + "version": "8.0.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3012,6 +665,7 @@ "bazel_package_name": "swiftpkg_lnextensionexecutor", "commit": "c0226dcd7d653d4c22dd16ccd72619c86b610c2d", "remote": "https://github.com/LeoNatan/LNExtensionExecutor", + "version": "1.3.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3030,6 +684,7 @@ "bazel_package_name": "swiftpkg_ton_api_swift", "commit": "d57d457cbaea081467ae65648e92fc6094ea342b", "remote": "https://github.com/tonkeeper/ton-api-swift", + "version": "0.1.7", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3048,6 +703,7 @@ "bazel_package_name": "swiftpkg_session_manager_swift", "commit": "67d5f7db655d02778861057fb280ecf47c923b09", "remote": "https://github.com/Web3Auth/session-manager-swift.git", + "version": "5.0.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3066,6 +722,7 @@ "bazel_package_name": "swiftpkg_tweetnacl_swiftwrap", "commit": "f8fd111642bf2336b11ef9ea828510693106e954", "remote": "https://github.com/bitmark-inc/tweetnacl-swiftwrap", + "version": "1.1.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3084,6 +741,7 @@ "bazel_package_name": "swiftpkg_swiftui_flow", "commit": "3086a602b98155eec28b4be79210d6cb1a43e339", "remote": "https://github.com/denis15yo/SwiftUI-Flow.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3102,6 +760,7 @@ "bazel_package_name": "swiftpkg_swiftystorekit", "commit": "9ce911639680113dac9b554d6243e406a9758ebe", "remote": "https://github.com/bizz84/SwiftyStoreKit.git", + "version": "0.16.4", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3118,8 +777,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_torus_utils_swift", - "commit": "eda55b8537a600e657d19d4c452c0a36f217883c", + "commit": "0ac2810afef07283ae324eb1a2b17c5d6f7511d6", "remote": "https://github.com/torusresearch/torus-utils-swift.git", + "version": "9.0.2", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3136,8 +796,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_factory", - "commit": "587995f7d5cc667951d635fbf6b4252324ba0439", + "commit": "f350e0d71ba241b392f70519a67e769d5e3858d4", "remote": "https://github.com/hmlongco/Factory.git", + "version": "2.4.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3156,6 +817,7 @@ "bazel_package_name": "swiftpkg_snapkit", "commit": "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", "remote": "https://github.com/SnapKit/SnapKit.git", + "version": "5.7.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3174,6 +836,7 @@ "bazel_package_name": "swiftpkg_ton_swift", "commit": "e08680928f8fd83319c47f1f48301a52ba502b8b", "remote": "https://github.com/denis15yo/ton-swift.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3192,6 +855,7 @@ "bazel_package_name": "swiftpkg_bigint", "commit": "0ed110f7555c34ff468e72e1686e59721f2b0da6", "remote": "https://github.com/attaswift/BigInt", + "version": "5.3.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3210,6 +874,7 @@ "bazel_package_name": "swiftpkg_core_swift", "commit": "6a36c9b16f84ed28ffbc27994ece3d974da35fdb", "remote": "https://github.com/denis15yo/core-swift.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3228,6 +893,7 @@ "bazel_package_name": "swiftpkg_walletconnectswiftv2", "commit": "3327c0a8c014b155534b91520554d812e2960077", "remote": "https://github.com/WalletConnect/WalletConnectSwiftV2.git", + "version": "1.20.3", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3246,6 +912,7 @@ "bazel_package_name": "swiftpkg_swiftimagereadwrite", "commit": "5596407d1cf61b953b8e658fa8636a471df3c509", "remote": "https://github.com/dagronf/SwiftImageReadWrite", + "version": "1.1.6", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3264,6 +931,7 @@ "bazel_package_name": "swiftpkg_tkey_ios", "commit": "c107450f0675351a9a1eaaefe60bcfa285ff1f9e", "remote": "https://github.com/tkey/tkey-ios.git", + "version": "0.2.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3282,6 +950,7 @@ "bazel_package_name": "swiftpkg_navigation_stack_backport", "commit": "66716ce9c31198931c2275a0b69de2fdaa687e74", "remote": "https://github.com/denis15yo/navigation-stack-backport.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3300,6 +969,7 @@ "bazel_package_name": "swiftpkg_starscream", "commit": "c6bfd1af48efcc9a9ad203665db12375ba6b145a", "remote": "https://github.com/daltoniam/Starscream.git", + "version": "4.0.8", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3316,26 +986,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_sdwebimage", - "commit": "8a1be70a625683bc04d6903e2935bf23f3c6d609", + "commit": "10d06f6a33bafae8c164fbfd1f03391f6d4692b3", "remote": "https://github.com/SDWebImage/SDWebImage.git", - "init_submodules": false, - "recursive_init_submodules": true, - "patch_args": [ - "-p0" - ], - "patch_cmds": [], - "patch_cmds_win": [], - "patch_tool": "", - "patches": [] - } - }, - "swiftpkg_subscriptionanalytics_ios": { - "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", - "ruleClassName": "swift_package", - "attributes": { - "bazel_package_name": "swiftpkg_subscriptionanalytics_ios", - "commit": "53bfc6c6f26322ec647b87c338a071714ac69420", - "remote": "git@bitbucket.org:mobyrix/subscriptionanalytics-ios.git", + "version": "5.20.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3354,6 +1007,7 @@ "bazel_package_name": "swiftpkg_swift_argument_parser", "commit": "41982a3656a71c768319979febd796c6fd111d5c", "remote": "https://github.com/apple/swift-argument-parser", + "version": "1.5.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3372,6 +1026,7 @@ "bazel_package_name": "swiftpkg_swift_collections", "commit": "671108c96644956dddcd89dd59c203dcdb36cec7", "remote": "https://github.com/apple/swift-collections", + "version": "1.1.4", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3390,6 +1045,7 @@ "bazel_package_name": "swiftpkg_swift_qrcode_generator", "commit": "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", "remote": "https://github.com/dagronf/swift-qrcode-generator", + "version": "1.0.3", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3408,6 +1064,7 @@ "bazel_package_name": "swiftpkg_curvelib.swift", "commit": "9f88bd5e56d1df443a908f7a7e81ae4f4d9170ea", "remote": "https://github.com/tkey/curvelib.swift", + "version": "1.0.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3426,6 +1083,7 @@ "bazel_package_name": "swiftpkg_bigdecimal", "commit": "04d17040e4615fbfda3a882b9881f6841f4bf557", "remote": "https://github.com/Zollerboy1/BigDecimal.git", + "version": "1.0.2", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3444,6 +1102,7 @@ "bazel_package_name": "swiftpkg_swift_openapi_urlsession", "commit": "9229842c63e9fc3bbd32c661d8274b4d9d8715f1", "remote": "https://github.com/apple/swift-openapi-urlsession.git", + "version": "0.3.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3460,8 +1119,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_assistant_ios", - "commit": "065294e32e5fb6a34fa08c9a34e3ba4f19ee8a17", + "commit": "970f0835a1b64becc8d6fbe0e9a381f04d1e14bd", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3480,6 +1140,7 @@ "bazel_package_name": "swiftpkg_fetch_node_details_swift", "commit": "4bd96c33ba8d02d9e27190c5c7cedf09cfdfd656", "remote": "https://github.com/torusresearch/fetch-node-details-swift.git", + "version": "6.0.3", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3498,6 +1159,7 @@ "bazel_package_name": "swiftpkg_swift_numerics", "commit": "0a5bc04095a675662cf24757cc0640aa2204253b", "remote": "https://github.com/apple/swift-numerics.git", + "version": "1.0.2", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3516,6 +1178,7 @@ "bazel_package_name": "swiftpkg_swift_http_types", "commit": "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", "remote": "https://github.com/apple/swift-http-types", + "version": "1.3.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3534,6 +1197,7 @@ "bazel_package_name": "swiftpkg_grdb.swift", "commit": "156d630c7a4175ddf4d529244f4672428cc6e2fc", "remote": "https://github.com/denis15yo/GRDB.swift.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3552,6 +1216,7 @@ "bazel_package_name": "swiftpkg_qrcode", "commit": "263f280d2c8144adfb0b6676109846cfc8dd552b", "remote": "https://github.com/WalletConnect/QRCode.git", + "version": "14.3.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3570,6 +1235,7 @@ "bazel_package_name": "swiftpkg_floatingpanel", "commit": "b6e8928b1a3ad909e6db6a0278d286c33cfd0dc3", "remote": "https://github.com/scenee/FloatingPanel", + "version": "2.8.6", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3588,6 +1254,7 @@ "bazel_package_name": "swiftpkg_r.swift", "commit": "4a0f8c97f1baa27d165dc801982c55bbf51126e5", "remote": "https://github.com/denis15yo/R.swift.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3606,6 +1273,7 @@ "bazel_package_name": "swiftpkg_keychain_swift", "commit": "d108a1fa6189e661f91560548ef48651ed8d93b9", "remote": "https://github.com/evgenyneu/keychain-swift.git", + "version": "20.0.0", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3624,6 +1292,7 @@ "bazel_package_name": "swiftpkg_cryptoswift", "commit": "678d442c6f7828def400a70ae15968aef67ef52d", "remote": "https://github.com/krzyzanowskim/CryptoSwift.git", + "version": "1.8.3", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3640,8 +1309,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_wallet_core", - "commit": "4af0ee33be559941fbda7d3519a9dd032006ab52", + "commit": "c4907d444673e7eb4c131b67ce2ced5b2d2cb09e", "remote": "https://github.com/trustwallet/wallet-core.git", + "version": "4.1.16", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3660,6 +1330,7 @@ "bazel_package_name": "swiftpkg_swift_openapi_runtime", "commit": "a51b3bd6f2151e9a6f792ca6937a7242c4758768", "remote": "https://github.com/apple/swift-openapi-runtime", + "version": "0.3.6", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3676,8 +1347,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_xcodeedit", - "commit": "017d23f71fa8d025989610db26d548c44cacefae", + "commit": "1e761a55dd8d73b4e9cc227a297f438413953571", "remote": "https://github.com/tomlokhorst/XcodeEdit", + "version": "2.11.1", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3694,8 +1366,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_wallet_ios", - "commit": "6470c253b57cc3dd47c5b088b7350138fbece800", + "commit": "6417219110dae50a9dd129c61795dc469ceb12d0", "remote": "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", + "version": "", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3714,6 +1387,7 @@ "bazel_package_name": "swiftpkg_rive_ios", "commit": "52ab8860c3b2264cc44757cb8fc24689f5e1b564", "remote": "https://github.com/rive-app/rive-ios.git", + "version": "5.11.3", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -3756,7 +1430,8 @@ }, "@@rules_swift~//swift:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "QgKzLzLwbmI9NGTblAH/DxdVM/oxwn6wSNO3wtXZiWM=", + "bzlTransitiveDigest": "YAPVHC4my/QNTeYIR+CnPAfhlQTV9I8LvJNhFSfWe+Q=", + "usagesDigest": "Vkbl7z8ypQL+SWKEf+654A82FjYtt+1IosUH4konhws=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -3915,7 +1590,8 @@ }, "@@rules_xcodeproj~//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "tSHwCORMSaTwtem/RoFY8getSILf68ZYVjUaC2RnlrQ=", + "bzlTransitiveDigest": "hLIQlNwmeIh1hiSFgOy6bAluh+3cnOtYQJNuVU83Cdk=", + "usagesDigest": "Fd0LbToHfzE3orok9aNu+QUYZnwlS1nePXhR6NSO+A4=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -3937,7 +1613,8 @@ }, "@@rules_xcodeproj~//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "tSHwCORMSaTwtem/RoFY8getSILf68ZYVjUaC2RnlrQ=", + "bzlTransitiveDigest": "hLIQlNwmeIh1hiSFgOy6bAluh+3cnOtYQJNuVU83Cdk=", + "usagesDigest": "jrpYdEGEUypUrIJLOkwRaCYJA1OEeJci9vZWdoWxgvs=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/Nicegram/NGCallRecorder/BUILD b/Nicegram/NGCallRecorder/BUILD new file mode 100644 index 00000000000..3d2741d7a6a --- /dev/null +++ b/Nicegram/NGCallRecorder/BUILD @@ -0,0 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NGCallRecorder", + module_name = "NGCallRecorder", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/ComponentFlow", + "//submodules/Display" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Nicegram/NGCallRecorder/Sources/RecordIndicatorView.swift b/Nicegram/NGCallRecorder/Sources/RecordIndicatorView.swift new file mode 100644 index 00000000000..a73ba0a14e1 --- /dev/null +++ b/Nicegram/NGCallRecorder/Sources/RecordIndicatorView.swift @@ -0,0 +1,85 @@ +import UIKit +import Display +import ComponentFlow + +public class RecordIndicatorView: UIView { + private let contentView = UIView() + private let label: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .systemFont(ofSize: 16.0, weight: .regular) + + return label + }() + private let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(bundleImageName: "RecordIndicator") + + return imageView + }() + private let textHeight: CGFloat = 20.0 + private let sideInset: CGFloat = 8.0 + private let verticalInset: CGFloat = 4.0 + private let iconSpacing: CGFloat = 4.0 + private var actualSize: CGSize = .zero + + override init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = true + + contentView.clipsToBounds = true + contentView.layer.cornerRadius = (textHeight + verticalInset * 2) / 2 + contentView.backgroundColor = UIColor(hexString: "FF453A") + + addSubview(contentView) + contentView.addSubview(imageView) + contentView.addSubview(label) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func animateIn() { + self.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + public func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } + + public func update( + text: String, + constrainedWidth: CGFloat = 75.0, + transition: ComponentTransition + ) -> CGSize { + label.text = text + + let iconSize = self.imageView.image?.size ?? CGSize(width: 20.0, height: 20.0) + + let maxSize = CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude) + let boundingRect = text.boundingRect( + with: maxSize, + options: .usesLineFragmentOrigin, + attributes: [.font: label.font ?? UIFont()], + context: nil + ) + let textSize = CGSize(width: ceil(max(boundingRect.width, 35.0)), height: textHeight) + + let size = CGSize(width: iconSize.width + iconSpacing + textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) + actualSize = size.width > actualSize.width ? size : actualSize + + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: actualSize)) + + transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((actualSize.height - iconSize.height) * 0.5) + 4.0, y: verticalInset + floorToScreenPixels((textSize.height - iconSize.height) * 0.5)), size: iconSize)) + + transition.setFrame(view: self.label, frame: CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: verticalInset), size: textSize)) + + return actualSize + } +} diff --git a/Nicegram/NGData/Sources/NGSettings.swift b/Nicegram/NGData/Sources/NGSettings.swift index 75d9d9a6028..065f97e1e15 100644 --- a/Nicegram/NGData/Sources/NGSettings.swift +++ b/Nicegram/NGData/Sources/NGSettings.swift @@ -1,4 +1,5 @@ import FeatPremium +import Postbox import Foundation import NGAppCache @@ -105,6 +106,15 @@ public struct NGSettings { @NGStorage(key: "hideStories", defaultValue: false) public static var hideStories: Bool + + @NGStorage(key: "recordAllCalls", defaultValue: false) + public static var recordAllCalls: Bool + + @NGStorage(key: "showFeedTab", defaultValue: false) + public static var showFeedTab: Bool + + @NGStorage(key: "feedPeer", defaultValue: [:]) + public static var feedPeer: [Int64: PeerId] } public struct NGWebSettings { @@ -135,9 +145,13 @@ public var VarNGSharedSettings = NGSharedSettings() public func isPremium() -> Bool { if #available(iOS 13.0, *) { +#if DEBUG + return true +#else return PremiumContainer.shared .getPremiumStatusUseCase() .hasPremiumOnDevice() +#endif } else { return false } diff --git a/Nicegram/NGEnv/Sources/NGEnv.swift b/Nicegram/NGEnv/Sources/NGEnv.swift index e4f4a3d26e9..291b7582489 100644 --- a/Nicegram/NGEnv/Sources/NGEnv.swift +++ b/Nicegram/NGEnv/Sources/NGEnv.swift @@ -2,12 +2,11 @@ import Foundation import BuildConfig public struct NGEnvObj: Decodable { - public let app_review_login_code: String + public let app_review_login_code_url: String public let app_review_login_phone: String public let premium_bundle: String public let ng_api_key: String public let ng_api_url: String - public let moby_key: String public let privacy_url: String public let terms_url: String public let referral_bot: String @@ -25,6 +24,8 @@ public struct NGEnvObj: Decodable { public let web3AuthBackupQuestion: String public let web3AuthClientId: String public let web3AuthVerifier: String + public let stonfiApiUrl: String + public let stonfiNicegramApiUrl: String } } diff --git a/Nicegram/NGRemoteConfig/Sources/RemoteConfigService.swift b/Nicegram/NGRemoteConfig/Sources/RemoteConfigService.swift index d814ca44dc8..444a9ef0411 100644 --- a/Nicegram/NGRemoteConfig/Sources/RemoteConfigService.swift +++ b/Nicegram/NGRemoteConfig/Sources/RemoteConfigService.swift @@ -11,6 +11,7 @@ public class RemoteConfigServiceImpl { // MARK: - Dependencies + private let remoteConfigFetchTask: RemoteConfigFetchTask private let firebaseRemoteConfig: FirebaseRemoteConfigService // MARK: - Logic @@ -35,34 +36,16 @@ public class RemoteConfigServiceImpl { private init(firebaseRemoteConfig: FirebaseRemoteConfigService) { self.firebaseRemoteConfig = firebaseRemoteConfig - } - - // MARK: - Private Functions - - @available(iOS 13.0, *) - private func startFetchTaskIfNeeded() -> Task { - if let task = task as? Task { - return task - } - - let task = Task { - await withCheckedContinuation { continuation in - firebaseRemoteConfig.prefetch { - continuation.resume() - } - } - } - - self.task = task - - return task + self.remoteConfigFetchTask = .init(firebaseRemoteConfig: firebaseRemoteConfig) } } extension RemoteConfigServiceImpl: RemoteConfigService { public func prefetch() { if #available(iOS 13.0, *) { - _ = startFetchTaskIfNeeded() + Task { + _ = await remoteConfigFetchTask.startIfNeeded() + } } else { firebaseRemoteConfig.prefetch(completion: {}) } @@ -102,8 +85,36 @@ extension RemoteConfigServiceImpl: RemoteConfig { @available(iOS 13.0, *) public func asyncGet(_ variable: RemoteVariable) async -> Payload { - let task = startFetchTaskIfNeeded() + let task = await remoteConfigFetchTask.startIfNeeded() _ = await task.value return self.get(variable) } } + +private actor RemoteConfigFetchTask { + private let firebaseRemoteConfig: FirebaseRemoteConfigService + + private var task: Any? + + init(firebaseRemoteConfig: FirebaseRemoteConfigService) { + self.firebaseRemoteConfig = firebaseRemoteConfig + } + + func startIfNeeded() -> Task { + if let task = task as? Task { + return task + } + + let task = Task { + await withCheckedContinuation { continuation in + firebaseRemoteConfig.prefetch { + continuation.resume() + } + } + } + + self.task = task + + return task + } +} diff --git a/Nicegram/NGResources/BUCK b/Nicegram/NGResources/BUCK new file mode 100644 index 00000000000..edb66f8809d --- /dev/null +++ b/Nicegram/NGResources/BUCK @@ -0,0 +1,41 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + + +apple_asset_catalog( + name = "NGUIAssets", + dirs = [ + "Images.xcassets", + ], + visibility = ["PUBLIC"], +) + +static_library( + name = "NGUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/AccountContext:AccountContext", + "//submodules/Display:Display#shared", + "//submodules/ItemListUI:ItemListUI", + "//submodules/Postbox:Postbox#shared", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/TelegramNotices:TelegramNotices", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", + "//submodules/TelegramPermissionsUI:TelegramPermissionsUI", + "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", + "//Nicegram/NGData:NGData", + "//Nicegram/NGLogging:NGLogging", + "//Nicegram/NGStrings:NGStrings", + "//Nicegram/NGIAP:NGIAP" + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/StoreKit.framework", + ], +) diff --git a/Nicegram/NGResources/BUILD b/Nicegram/NGResources/BUILD new file mode 100644 index 00000000000..e9952387dfe --- /dev/null +++ b/Nicegram/NGResources/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + + +filegroup( + name = "NGImagesAssets", + srcs = glob(["Images.xcassets/**"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "NGResources", + module_name = "NGResources", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Nicegram/NGUI/Images.xcassets/Contents.json b/Nicegram/NGResources/Images.xcassets/Contents.json similarity index 100% rename from Nicegram/NGUI/Images.xcassets/Contents.json rename to Nicegram/NGResources/Images.xcassets/Contents.json diff --git a/Nicegram/NGUI/Images.xcassets/CopyForward.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/CopyForward.imageset/Contents.json similarity index 100% rename from Nicegram/NGUI/Images.xcassets/CopyForward.imageset/Contents.json rename to Nicegram/NGResources/Images.xcassets/CopyForward.imageset/Contents.json diff --git a/Nicegram/NGUI/Images.xcassets/CopyForward.imageset/ic_lt_forwardascopy.pdf b/Nicegram/NGResources/Images.xcassets/CopyForward.imageset/ic_lt_forwardascopy.pdf similarity index 100% rename from Nicegram/NGUI/Images.xcassets/CopyForward.imageset/ic_lt_forwardascopy.pdf rename to Nicegram/NGResources/Images.xcassets/CopyForward.imageset/ic_lt_forwardascopy.pdf diff --git a/Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/Contents.json similarity index 100% rename from Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/Contents.json rename to Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/Contents.json diff --git a/Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_1x.png b/Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_1x.png similarity index 100% rename from Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_1x.png rename to Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_1x.png diff --git a/Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_2x.png b/Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_2x.png similarity index 100% rename from Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_2x.png rename to Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_2x.png diff --git a/Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_3x.png b/Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_3x.png similarity index 100% rename from Nicegram/NGUI/Images.xcassets/NGTranslateIcon.imageset/language_3x.png rename to Nicegram/NGResources/Images.xcassets/NGTranslateIcon.imageset/language_3x.png diff --git a/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/Contents.json new file mode 100644 index 00000000000..3b27724d82c --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "record_indicator.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/record_indicator.pdf b/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/record_indicator.pdf new file mode 100644 index 00000000000..b453c7a5025 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/RecordIndicator.imageset/record_indicator.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/Contents.json new file mode 100644 index 00000000000..1fb31a0f0e6 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "record_save.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/record_save.pdf b/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/record_save.pdf new file mode 100644 index 00000000000..06fa527d3b4 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/RecordSave.imageset/record_save.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/Contents.json new file mode 100644 index 00000000000..fa47a27a078 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "record_start.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/record_start.pdf b/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/record_start.pdf new file mode 100644 index 00000000000..6135a58c8a2 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/RecordStart.imageset/record_start.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/Contents.json new file mode 100644 index 00000000000..a68e2d2bbd3 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "record_stop.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/record_stop.pdf b/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/record_stop.pdf new file mode 100644 index 00000000000..0af82670075 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/RecordStop.imageset/record_stop.pdf differ diff --git a/Nicegram/NGUI/Images.xcassets/SaveToCloud.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/SaveToCloud.imageset/Contents.json similarity index 100% rename from Nicegram/NGUI/Images.xcassets/SaveToCloud.imageset/Contents.json rename to Nicegram/NGResources/Images.xcassets/SaveToCloud.imageset/Contents.json diff --git a/Nicegram/NGUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf b/Nicegram/NGResources/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf similarity index 100% rename from Nicegram/NGUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf rename to Nicegram/NGResources/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf diff --git a/Nicegram/NGResources/Images.xcassets/feed.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/feed.imageset/Contents.json new file mode 100644 index 00000000000..d25acefcab4 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/feed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "feed.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nicegram/NGResources/Images.xcassets/feed.imageset/feed.pdf b/Nicegram/NGResources/Images.xcassets/feed.imageset/feed.pdf new file mode 100644 index 00000000000..b95392e84f0 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/feed.imageset/feed.pdf differ diff --git a/Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/Contents.json similarity index 100% rename from Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/Contents.json rename to Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/Contents.json diff --git a/Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/logo-nicegram-dark.pdf b/Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/logo-nicegram-dark.pdf similarity index 100% rename from Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/logo-nicegram-dark.pdf rename to Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/logo-nicegram-dark.pdf diff --git a/Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/logo-nicegram.pdf b/Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/logo-nicegram.pdf similarity index 100% rename from Nicegram/NGUI/Images.xcassets/logo-nicegram.imageset/logo-nicegram.pdf rename to Nicegram/NGResources/Images.xcassets/logo-nicegram.imageset/logo-nicegram.pdf diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/Contents.json b/Nicegram/NGResources/Images.xcassets/ng-settings/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/ng-settings/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/Contents.json new file mode 100644 index 00000000000..41a854debf6 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "ai-chatbot-light.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ai-chatbot-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-dark.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-dark.pdf new file mode 100644 index 00000000000..16b5895807b Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-dark.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-light.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-light.pdf new file mode 100644 index 00000000000..bbe61431f6b Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/ai-chatbot.imageset/ai-chatbot-light.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/Contents.json new file mode 100644 index 00000000000..3751cb9300c --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "premium-light.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "premium-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-dark.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-dark.pdf new file mode 100644 index 00000000000..b5705546dc7 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-dark.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-light.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-light.pdf new file mode 100644 index 00000000000..9e433ce81ea Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/premium.imageset/premium-light.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/Contents.json new file mode 100644 index 00000000000..f3284704c3a --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "settings-light.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "settings-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-dark.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-dark.pdf new file mode 100644 index 00000000000..18e10dcf302 Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-dark.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-light.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-light.pdf new file mode 100644 index 00000000000..6ef7a81cb6d Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/settings.imageset/settings-light.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/Contents.json b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/Contents.json new file mode 100644 index 00000000000..27205bc8574 --- /dev/null +++ b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "wallet-light.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "wallet-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-dark.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-dark.pdf new file mode 100644 index 00000000000..3832516f59d Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-dark.pdf differ diff --git a/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-light.pdf b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-light.pdf new file mode 100644 index 00000000000..fc3d654574f Binary files /dev/null and b/Nicegram/NGResources/Images.xcassets/ng-settings/wallet.imageset/wallet-light.pdf differ diff --git a/Nicegram/NGResources/Sources/dummy.swift b/Nicegram/NGResources/Sources/dummy.swift new file mode 100644 index 00000000000..139597f9cb0 --- /dev/null +++ b/Nicegram/NGResources/Sources/dummy.swift @@ -0,0 +1,2 @@ + + diff --git a/Nicegram/NGUI/BUILD b/Nicegram/NGUI/BUILD index 1df335e4c95..cdb43d92aab 100644 --- a/Nicegram/NGUI/BUILD +++ b/Nicegram/NGUI/BUILD @@ -1,12 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -filegroup( - name = "NGUIAssets", - srcs = glob(["Images.xcassets/**"]), - visibility = ["//visibility:public"], -) - swift_library( name = "NGUI", module_name = "NGUI", diff --git a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/Contents.json b/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/Contents.json deleted file mode 100644 index 7a193e7f25c..00000000000 --- a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "aichat.avatar@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "aichat.avatar@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@2x.png b/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@2x.png deleted file mode 100644 index c67bc8c8efa..00000000000 Binary files a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@2x.png and /dev/null differ diff --git a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@3x.png b/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@3x.png deleted file mode 100644 index 6b741109eed..00000000000 Binary files a/Nicegram/NGUI/Images.xcassets/ng.aichat.avatar.imageset/aichat.avatar@3x.png and /dev/null differ diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index 963f3cefc76..b34db79e8d9 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -36,6 +36,7 @@ import NGDoubleBottom import NGQuickReplies import NGRemoteConfig import NGStats +import NGUtils fileprivate let LOGTAG = extractNameFromPath(#file) @@ -80,7 +81,7 @@ private enum EasyToggleType { case showProfileId case showRegDate case hideReactions - case hideStories + case hideStories } @@ -92,6 +93,7 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { case showCallsTab(String, Bool) case showNicegramTab case showTabNames(String, Bool) + case showFeedTab(String, Bool) case pinnedChatsHeader case pinnedChat(Int32, PinnedChat) @@ -127,7 +129,7 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .TabsHeader, .showContactsTab, .showCallsTab, .showNicegramTab, .showTabNames: + case .TabsHeader, .showContactsTab, .showCallsTab, .showNicegramTab, .showTabNames, .showFeedTab: return NicegramSettingsControllerSection.Tabs.rawValue case .FoldersHeader, .foldersAtBottom, .foldersAtBottomNotice: return NicegramSettingsControllerSection.Folders.rawValue @@ -173,6 +175,9 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { case .showTabNames: return 1600 + case .showFeedTab: + return 1650 + case .FoldersHeader: return 1700 @@ -226,6 +231,7 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { return 6002 case .shareDataNote: return 6010 + } } @@ -267,6 +273,13 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { } else { return false } + + case let .showFeedTab(lhsText, lhsVar0Bool): + if case let .showFeedTab(rhsText, rhsVar0Bool) = rhs, lhsText == rhsText, lhsVar0Bool == rhsVar0Bool { + return true + } else { + return false + } case let .FoldersHeader(lhsText): if case let .FoldersHeader(rhsText) = rhs, lhsText == rhsText { @@ -456,6 +469,13 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { arguments.presentController(controller, nil) }) + case let .showFeedTab(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: section, style: .blocks, updated: { value in + ngLog("[showFeedTab] invoked with \(value)", LOGTAG) + NGSettings.showFeedTab = value + arguments.updateTabs() + }) + case let .FoldersHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: section) @@ -513,8 +533,14 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { NGSettings.showRegDate = value case .hideReactions: VarSystemNGSettings.hideReactions = value + if value { + sendUserSettingsAnalytics(with: .hideReactionsOn) + } case .hideStories: NGSettings.hideStories = value + if value { + sendUserSettingsAnalytics(with: .hideStoriesOn) + } } }) case let .unblockHeader(text): @@ -631,6 +657,10 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat l("NiceFeatures.Tabs.ShowNames"), NGSettings.showTabNames )) + entries.append(.showFeedTab( + l("Show Feed Tab"), + NGSettings.showFeedTab + )) entries.append(.FoldersHeader(l("NiceFeatures.Folders.Header"))) entries.append(.foldersAtBottom( @@ -701,7 +731,7 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.easyToggle(toggleIndex, .hideStories, l("NicegramSettings.HideStories"), NGSettings.hideStories)) toggleIndex += 1 - + if let sharingSettings { entries.append( .shareBotsData( @@ -816,19 +846,23 @@ public func nicegramSettingsController(context: AccountContext, accountsContexts controller?.view.window?.rootViewController } updateTabsImpl = { + updateTabs(with: context) + } + return controller +} + +public func updateTabs(with context: AccountContext) { + _ = updateCallListSettingsInteractively(accountManager: context.sharedContext.accountManager) { settings in + var settings = settings + settings.showTab = !settings.showTab + return settings + }.start(completed: { _ = updateCallListSettingsInteractively(accountManager: context.sharedContext.accountManager) { settings in var settings = settings settings.showTab = !settings.showTab return settings }.start(completed: { - _ = updateCallListSettingsInteractively(accountManager: context.sharedContext.accountManager) { settings in - var settings = settings - settings.showTab = !settings.showTab - return settings - }.start(completed: { - ngLog("Tabs refreshed", LOGTAG) - }) + ngLog("Tabs refreshed", LOGTAG) }) - } - return controller + }) } diff --git a/Nicegram/NGUI/Sources/PremiumController.swift b/Nicegram/NGUI/Sources/PremiumController.swift index a8b5f37a2f2..65d10c93058 100644 --- a/Nicegram/NGUI/Sources/PremiumController.swift +++ b/Nicegram/NGUI/Sources/PremiumController.swift @@ -19,6 +19,7 @@ import AccountContext import TelegramNotices import NGData import NGStrings +import NGUtils private struct SelectionState: Equatable { } @@ -47,6 +48,7 @@ private enum premiumControllerSection: Int32 { case manageFilters case other case speechToText + case calls case test } @@ -83,6 +85,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { case ignoretr(PresentationTheme, String) case useOpenAi(Bool) + + case recordAllCalls(String, Bool) var section: ItemListSectionId { switch self { @@ -100,9 +104,12 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return premiumControllerSection.test.rawValue case .useOpenAi: return premiumControllerSection.speechToText.rawValue + case .recordAllCalls: + return premiumControllerSection.calls.rawValue } - } +// case .recordAllCalls: +// NGSettings.recordAllCalls = value var stableId: Int32 { switch self { @@ -132,6 +139,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return 12000 case .useOpenAi: return 13000 + case .recordAllCalls: + return 14000 case .testButton: return 999999 } @@ -231,6 +240,12 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } else { return false } + case let .recordAllCalls(lhsText, lhsBool): + if case let .recordAllCalls(rhsText, rhsBool) = rhs, lhsText == rhsText, lhsText == rhsText { + return true + } else { + return false + } } } @@ -294,6 +309,13 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } } }) + case let .recordAllCalls(title, value): + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + NGSettings.recordAllCalls = value + if value { + sendUserSettingsAnalytics(with: .recordAllCallsOn) + } + }) } } } @@ -303,13 +325,14 @@ private func premiumControllerEntries(presentationData: PresentationData, useOpe var entries: [PremiumControllerEntry] = [] let theme = presentationData.theme - let strings = presentationData.strings entries.append(.rememberFolderOnExit(theme, l("Premium.rememberFolderOnExit"), NGSettings.rememberFolderOnExit)) entries.append(.onetaptr(theme, l("Premium.OnetapTranslate"), NGSettings.oneTapTr)) entries.append(.ignoretr(theme, l("Premium.IgnoreTranslate.Title"))) entries.append(.useOpenAi(useOpenAi)) + + entries.append(.recordAllCalls(l("Premium.RecordAllCalls"), NGSettings.recordAllCalls)) #if DEBUG entries.append(.testButton(theme, "TEST")) diff --git a/Nicegram/NGUtils/Sources/Analytics/CallRecorderAnalytics.swift b/Nicegram/NGUtils/Sources/Analytics/CallRecorderAnalytics.swift new file mode 100644 index 00000000000..80ddf8fcb90 --- /dev/null +++ b/Nicegram/NGUtils/Sources/Analytics/CallRecorderAnalytics.swift @@ -0,0 +1,17 @@ +import Foundation +import NGAnalytics + +public enum CallrecorderAnalyticsEvent: String { + case startAuto = "call_recorder_start_auto" + case start = "call_recorder_start" + case end = "call_recorder_end" + case error = "call_recorder_error" +} + +public func sendCallRecorderAnalytics(with event: CallrecorderAnalyticsEvent) { + let analyticsManager = AnalyticsContainer.shared.analyticsManager() + analyticsManager.trackEvent( + event.rawValue, + params: [:] + ) +} diff --git a/Nicegram/NGUtils/Sources/Analytics/UserSettingsAnalytics.swift b/Nicegram/NGUtils/Sources/Analytics/UserSettingsAnalytics.swift new file mode 100644 index 00000000000..0e366440601 --- /dev/null +++ b/Nicegram/NGUtils/Sources/Analytics/UserSettingsAnalytics.swift @@ -0,0 +1,18 @@ +import Foundation +import NGAnalytics + +public enum UserSettingsAnalyticsEvent: String { + case recordAllCallsOn = "user_settings_record_all_calls_on" + case hideStoriesOn = "user_settings_hide_stories_on" + case hideReactionsOn = "user_settings_hide_reactions_on" +// case openSameFolderAfterExitOn = "user_settings_open_same_folder_after_exit_on" +// case hideMessagesPreviewInFoldersOn = "user_settings_hide_messages_preview_in_folders_on" +} + +public func sendUserSettingsAnalytics(with event: UserSettingsAnalyticsEvent) { + let analyticsManager = AnalyticsContainer.shared.analyticsManager() + analyticsManager.trackEvent( + event.rawValue, + params: [:] + ) +} diff --git a/Nicegram/NGUtils/Sources/Peer.swift b/Nicegram/NGUtils/Sources/Peer.swift index f9c5249dc1e..210dba74f5e 100644 --- a/Nicegram/NGUtils/Sources/Peer.swift +++ b/Nicegram/NGUtils/Sources/Peer.swift @@ -1,16 +1,34 @@ import Postbox import TelegramCore +public extension PeerId { + func ng_toInt64() -> Int64 { + let originalId = id._internalGetInt64Value() + + var stringId = "\(originalId)" + if namespace == Namespaces.Peer.CloudChannel { + stringId = "-100\(stringId)" + } + + return Int64(stringId) ?? originalId + } +} + public func extractPeerId(peer: Peer) -> Int64 { - let enginePeer = EnginePeer(peer) - - let idText: String - switch enginePeer { - case .user, .legacyGroup, .secretChat: - idText = "\(peer.id.id._internalGetInt64Value())" - case .channel: - idText = "-100\(peer.id.id._internalGetInt64Value())" + peer.id.ng_toInt64() +} + +public func getMembersCount(cachedPeerData: CachedPeerData?) -> Int? { + guard let cachedPeerData else { + return nil } - return Int64(idText) ?? peer.id.id._internalGetInt64Value() + switch cachedPeerData { + case let channel as CachedChannelData: + return channel.participantsSummary.memberCount.flatMap(Int.init) + case let group as CachedGroupData: + return group.participants?.participants.count + default: + return nil + } } diff --git a/Package.resolved b/Package.resolved index fe27f79fce3..21ee780edc2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/hmlongco/Factory.git", "state" : { - "revision" : "587995f7d5cc667951d635fbf6b4252324ba0439", - "version" : "2.3.2" + "revision" : "f350e0d71ba241b392f70519a67e769d5e3858d4", + "version" : "2.4.1" } }, { @@ -114,7 +114,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "master", - "revision" : "73622cebd67fd56abf014708d5da860f0773c66c" + "revision" : "df0f5d6db8a979f215d18b05e889e5c66d64fe28" } }, { @@ -123,7 +123,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "state" : { "branch" : "master", - "revision" : "43356c38f071208ba27bb4b15617b86c15ca1cd4" + "revision" : "6417219110dae50a9dd129c61795dc469ceb12d0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609", - "version" : "5.19.7" + "revision" : "10d06f6a33bafae8c164fbfd1f03391f6d4692b3", + "version" : "5.20.0" } }, { @@ -198,15 +198,6 @@ "version" : "4.0.8" } }, - { - "identity" : "subscriptionanalytics-ios", - "kind" : "remoteSourceControl", - "location" : "git@bitbucket.org:mobyrix/subscriptionanalytics-ios.git", - "state" : { - "revision" : "53bfc6c6f26322ec647b87c338a071714ac69420", - "version" : "0.4.3" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -329,8 +320,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/torusresearch/torus-utils-swift.git", "state" : { - "revision" : "eda55b8537a600e657d19d4c452c0a36f217883c", - "version" : "9.0.1" + "revision" : "0ac2810afef07283ae324eb1a2b17c5d6f7511d6", + "version" : "9.0.2" } }, { @@ -347,8 +338,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/trustwallet/wallet-core.git", "state" : { - "revision" : "4af0ee33be559941fbda7d3519a9dd032006ab52", - "version" : "4.1.9" + "revision" : "c4907d444673e7eb4c131b67ce2ced5b2d2cb09e", + "version" : "4.1.16" } }, { @@ -365,8 +356,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tomlokhorst/XcodeEdit", "state" : { - "revision" : "017d23f71fa8d025989610db26d548c44cacefae", - "version" : "2.10.2" + "revision" : "1e761a55dd8d73b4e9cc227a297f438413953571", + "version" : "2.11.1" } } ], diff --git a/Random.txt b/Random.txt index 397d9dd9335..3578a816e4e 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -3a64b94cc76109006741756f85403c85 +3a64b94cc76109006741756f85403c86 diff --git a/Telegram/BUILD b/Telegram/BUILD index 7e4a273cdc2..51e69f56d3d 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -388,9 +388,12 @@ objc_library( NGRESOURCES = [ ":FirebaseRemoteConfigDefaults", ":GoogleService-Info", - "//Nicegram/NGUI:NGUIAssets", + "//Nicegram/NGResources:NGImagesAssets" +] + +NGRESOURCES_DEPS = [ + "//Nicegram/NGResources:NGResources" ] -NGRESOURCES_DEPS = ["//Nicegram/NGUI:NGUI"] swift_library( name = "Lib", diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index bb7f96225de..362baa1c121 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1710,7 +1710,7 @@ private final class NotificationServiceHandler { } else if let file = media as? TelegramMediaFile { resource = file.resource for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { fetchSize = preloadSize.flatMap(Int64.init) } } diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings deleted file mode 100644 index 739ba8e06d1..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,144 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "بوتات"; -"ChatFilter.Channels" = "قنوات"; -"ChatFilter.Private" = "مستخدمين"; -"ChatFilter.Groups" = "مجموعات"; -"ChatFilter.Unread" = "غير مقروءة"; -"ChatFilter.Unmuted" = "غير مكتومة"; -"ChatFilter.Favourites" = "المفضلات"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "تبويبات"; -"NiceFeatures.Tabs.ShowContacts" = "إظهار تبويب جهات الاتصال"; -"NiceFeatures.ChatScreen.Header" = "شاشة الدردشة"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "الملصقات المتحركة"; -"NiceFeatures.useBackCam" = "استخدام الكاميرا الخلفية (فيديوهات مستديرة)"; - -"NiceFeatures.Folders.Header" = "المجلدات"; -"NiceFeatures.Folders.TgFolders" = "المجلدات في الأسفل"; -"NiceFeatures.Folders.TgFolders.Notice" = "قائمة مجلدات بأسلوب iOS"; - -"NiceFeatures.RoundVideos.Header" = "فيديوهات مستديرة"; -"NiceFeatures.RoundVideos.UseRearCamera" = "البدء بالكاميرا الخلفية"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "الحفظ في السحابة"; - -/*Open Pin*/ -"Chat.OpenPin" = "عرض المثبت"; -"ChatFilter.Admin" = "مدير"; -"NiceFeatures.Notifications.HideNotifyAccount" = "إخفاء الحساب من الاشعارات"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "بدلًا من «المستخدم← الحساب» سترى فقط «المستخدم» للحسابات المتعددة فقط."; -"NiceFeatures.Notifications.Fix" = "تعطيل الأشعارات غير المرغوب فيها"; -"NiceFeatures.Notifications.FixNotice" = "مفيد إذا حصلت على إشعارات من دردشات صامتة.\nالرد من الإشعارات، تسميات الحساب، ومعاينات الوسائط لن تكون متاحة."; -"NiceFeatures.Filters.Header" = "الفلاتر (التبويبات)"; -"NiceFeatures.Filters.Notice" = "حدد عدد التبويبات المخصصة.\nأنقر مطولًا على التبويب لتغيير الفلتر."; -"NiceFeatures.Filters.ShowBadge" = "إظهار الشارات (الفلاتر)"; -"NiceFeatures.UseClassicInfoUi" = "استخدام واجهة معلومات الدردشة الكلاسيكية"; - -/*Common*/ -"Common.ExitNow" = "خروج الآن"; -"Common.Later" = "لاحقاً"; -"Common.RestartRequired" = "يجب إعادة التشغيل!"; -"NiceFeatures.Tabs.ShowNames" = "إظهار أسماء التبويبات"; -"Chat.ForwardAsCopy" = "إعادة توجيه كنسخة"; - -/*Folder*/ -"Folder.DefaultName" = "المجلد"; -"Folder.New" = "مجلد جديد"; -"Folder.Created" = "تم إنشاء المجلد"; -"Folder.AddToExisting" = "إضافة إلى مجلد جديد"; -"Folder.Updated" = "تم تحديث المجلد"; -"Folder.Create" = "إنشاء مجلد..."; -"Folder.Create.Name" = "إسم المجلد"; -"Folder.Create.Placeholder" = "المجلد..."; -"Folder.LimitExceeded" = "عفوًا، لا يمكنك إنشاء أكثر من 3 مجلدات مخصصة.\nالمزيد من المجلدات متوفرة في Premium."; -"NiceFeatures.HideNumber" = "إخفاء الرقم من الإعدادات"; - -/*NGWeb*/ -"NGWeb.Blocked" = "غير متوفر في Nicegram ويرجع السبب لقواعد AppStore التوجيهية"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "رجاءً، استخدم «%1» لضبط المتصفح الافتراضي"; -"NiceFeatures.Browser.Header" = "عناوين المواقع"; -"NiceFeatures.Browser.UseBrowser" = "فتح الروابط في المتصفح"; -"NiceFeatures.Browser.UseBrowserNotice" = "سيفتح Nicegram الروابط في المتصفح الخارجي بدلًا من التطبيق. يجب تثبيت المتصفح المحدد."; -"ChatFilter.Missed" = "فائتة"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "دردشات مثبتة غير محدودة"; -"Premium.SyncPins" = "مزامنة الدردشات المثبتة"; -"Premium.SyncPins.Notice.ON" = "إذا قمت بتغيير دردشات مثبتة في تطبيق آخر، فإنها ستتغير في Nicegram."; -"Premium.SyncPins.Notice.OFF" = "إذا قمت بتغير دردشات مثبتة في تطبيق آخر، فإنها لن تتغير في Nicegram."; -"Premium.Missed.Header" = "الرسائل الفائتة"; -"Premium.Missed" = "أخبرني عند ورود رسائل جديدة"; -"Premium.Missed.Notice" = "عند فتح التطبيق بعد تأخير طويل (النوم والدراسة وما إلى ذلك)، سيقوم Nicegram بإعلامك بالرسائل والإشارات الخاصة الغير المقروءة."; -"Folder.DeleteAsk" = "حذف المجلد"; -"Folder.NeedPremium" = "هذا المجلد متاح فقط مع Premium. يمكنك الحصول على Premium أو حذف المجلد عن طريق السحب."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "الأسئلة الشاسعة حول Nicegram"; -"Common.FAQ.Intro" = "يرجى أن يكون في علمك بأن دعم Nicegram يتم بواسطة المطور والمجتمع فقط.\n\nأولًا، ألقى نظرة على الأسئلة الشاسعة بخصوص Nicegram يتضمن نصائح مهمة في استكشاف الأخطاء وإجابات على معظم الأسئلة."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "ميزات فريدة لا يمكنك رفضها!"; -"IAP.Premium.Activated" = "تم تفعيل Premium!"; -"IAP.Common.Restore" = "استعادة عمليات الشراء"; -"IAP.Common.CantPay" = "عفوًا، ولكنك لا تستطيع القيام بعمليات الشراء بسبب قيود في جهازك أو حسابك."; -"IAP.Common.ErrorFetch" = "عفوًا، لا يمكن جلب عمليات الشراء من App Store."; -"IAP.Common.Congrats" = "مبروك!"; -"IAP.Common.ValidateError" = "عفوًا، لا يمكن التحقق من صحة عملة الشراء الخاص بك."; -"IAP.Common.Connecting" = "يتم الأتصال بالمتجر..."; -"Premium.OnetapTranslate" = "زر الترجمة الفوري"; -"Premium.IgnoreTranslate.Title" = "اللغات المتجاهلة"; -"Premium.IgnoreTranslate.Header" = "زر ترجمة الفوري سوف يعطّل للغات المختارة ادناه."; - -/*Manage Filters*/ -"ManageFilters.Title" = "إدارة التبويبات"; -"ManageFilters.Header" = "ضبط الفلاتر المتاحة"; - -"Messages.Translate" = "ترجمة"; -"Messages.UndoTranslate" = "التراجع عن الترجمة"; -"Messages.TranslateError" = "عفوًا، الترجمة غير متوفرة."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "الحصول على تاريخ التسجيل"; -"NGLab.RegDate.Title" = "تاريخ التسجيل"; -"NGLab.RegDate.Notice" = "هذا تاريخ تقريبي"; -"NGLab.RegDate.MenuItem" = "مسجّل"; -"NGLab.RegDate.FetchError" = "عذرًا، لا يمكن الحصول على تاريخ التسجيل."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "مزامنة الإعدادت والمجلدات مع iCloud"; -"NiceFeatures.BackupSettings" = "إعدادت النسخ الاحتياطي والمجلدات"; -"NiceFeatures.BackupSettings.Notice" = "إنشاء ملف النسخ الاحتياطي. انقر على ملف الاستعادة."; -"NiceFeatures.BackupSettings.Done" = "تم إرسال النسح الاحتياطي إلى الرسائل المحفوظة"; -"NiceFeatures.BackupSettings.Error" = "خطا في إنشاء نسخة الاحتياط"; - -"NiceFeatures.RestoreSettings.Confirm" = "هل أنت متأكد من أنك تودّ استعادة المجلدات والإعدادت من الملف؟\n⚠️سيتم تجاهل البيانات الحالية"; -"NiceFeatures.RestoreSettings.Done" = "تمت استعادة المجلدات والإعدادات بنجاح"; -"NiceFeatures.RestoreSettings.Error" = "خطأ في استعادة الإعدادت. قد يكون الملف تالفًا"; - -/*Preview Mode*/ -"Gmod.Restricted" = "لا يمكنك إرسال رسائل في وضع المعاينة."; -"Gmod.Unavailable" = "وضع المعاينة غير متوفر"; -"Gmod.Disable.Notice" = "سيتم تعيين ظهورك الأخير إلى «%1»"; -"Gmod" = "وضع المعاينة"; -"Gmod.Enable" = "تمكين وضع المعاينة؟"; -"Gmod.Disable" = "تعطيل وضع المعاينة؟"; -"Gmod.Notice" = "سيتم إخفاء حالة اتصالك بالأنترنت عن الجميع عن طريق إعدادت الخصوصية في تيليجرام.\nسيُظهر التطبيق تحذيرًا إذا كنت تدخل إلى محادثة خاصة.\nقد يتم الكشف عن اتصالك بالأنترنيت إذا قمت بإرسال أو كتابة أي رسالة."; - -"SendWithKb" = "إرسال مع زر «إدخال»"; -"NiceFeatures.ShowGmodIcon" = "إظهار أيقونة وضع المعاينة"; -"Gmod.OpenChatQ" = "فتح الدردشة؟"; -"Gmod.OpenChatNotice" = "سيتم وضع علامة \"مقروءة\"، ولكن يمكنك معاينة الدردشة بنقرة طويلة (اللمس بقوة)"; -"Gmod.OpenChatBtn" = "أجل، افتح الدردشة"; -"Gmod.DisableBtn" = "تعطيل التحذيرات"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "سيتم اخفاء رقمك من التطبيق فقط. لأخفاءهُ من المستخدمين الآخرين، يرجى استخدام إعدادات الخصوصية."; -"Premium.OnetapTranslate.LowPower" = "التعرف واطئ الإستهلاك"; -"Premium.OnetapTranslate.LowPower.Notice" = "التطبيق يحدد لغات كل رسالة في جهازك. ويتطلب اداء عالي من الجهاز و بالتالي إستهلاك البطارية."; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/cs.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/cs.lproj/NiceLocalizable.strings deleted file mode 100644 index f8e93acc8fa..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/cs.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,81 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bots"; -"ChatFilter.Channels" = "Channels"; -"ChatFilter.Private" = "Users"; -"ChatFilter.Groups" = "Groups"; -"ChatFilter.Unread" = "Unread"; -"ChatFilter.Unmuted" = "Unmuted"; -"ChatFilter.Favourites" = "Favourites"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "TABS"; -"NiceFeatures.Tabs.ShowContacts" = "Show Contacts Tab"; -"NiceFeatures.ChatScreen.Header" = "CHAT SCREEN"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animated Stickers"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Save to Cloud"; - -/*Open Pin*/ -"Chat.OpenPin" = "Show Pin"; -"ChatFilter.Admin" = "Admin"; -"NiceFeatures.Notifications.Fix" = "Disable Unwanted Notifications"; -"NiceFeatures.Notifications.FixNotice" = "Useful if you get notifications from muted chats.\nReply from notifications, account labels and media previews will be unavailable."; -"NiceFeatures.Filters.Header" = "FILTERS (TABS)"; -"NiceFeatures.Filters.Notice" = "Select the number of custom tabs.\nLongtap on tab to change filter."; - -/*Common*/ -"Common.ExitNow" = "Exit Now"; -"Common.Later" = "Later"; -"Common.RestartRequired" = "Restart Required!"; -"NiceFeatures.Tabs.ShowNames" = "Show Tab Names"; -"Chat.ForwardAsCopy" = "Forward As Copy"; - -/*Folder*/ -"Folder.DefaultName" = "Folder"; -"Folder.New" = "New Folder"; -"Folder.Created" = "Folder created"; -"Folder.AddToExisting" = "Add to Existing Folder"; -"Folder.Updated" = "Folder updated"; -"Folder.Create" = "Create Folder..."; -"Folder.Create.Name" = "Folder Name"; -"Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Sorry, you can create no more then 3 custom folders.\nMore folders are available in Premium."; -"NiceFeatures.HideNumber" = "Hide phone in settings"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Unavailable in Nicegram due to AppStore Guidelines"; - -/*Browser*/ -"NiceFeatures.Browser.Header" = "URLs"; -"NiceFeatures.Browser.UseBrowser" = "Open links in browser"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram will open links in external browser instead of in-app. Selected browser must be installed."; -"ChatFilter.Missed" = "Missed"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "UNLIMITED PINNED CHATS"; -"Premium.SyncPins" = "Sync Pinned Chats"; -"Premium.SyncPins.Notice.ON" = "If you change pinned chats in other client, they WILL CHANGE in Nicegram."; -"Premium.SyncPins.Notice.OFF" = "If you change pinned chats in other client, they WILL NOT CHANGE in Nicegram."; -"Premium.Missed.Header" = "Missed messages"; -"Premium.Missed" = "Notify on missed messages"; -"Premium.Missed.Notice" = "When you open App after long delay (sleeping, studying, etc.), Nicegram will notify you about unread private messages and mentions."; -"Folder.DeleteAsk" = "Delete Folder"; -"Folder.NeedPremium" = "This Folder is only available with Premium. You can get Premium or delete Folder with swipe."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Please note that Nicegram Support is done by the only one developer and community.\n\nFirstly, take a look at the Nicegram FAQ: it has important troubleshooting tips and answers to most questions."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Unique features you can't refuse!"; -"IAP.Premium.Features" = "Unlimited Folders\nUnlimited Pinned Chats\nMissed messages Filter\n«New Messages» digest on App launch"; -"IAP.Premium.Activated" = "Premium Activated!"; -"IAP.Common.Restore" = "Restore Purchases"; -"IAP.Common.CantPay" = "Sorry, but You can't make purchases because of your device or account restrictions."; -"IAP.Common.ErrorFetch" = "Sorry, сan't fetch App Store purchases."; -"IAP.Common.Congrats" = "Congratulations!"; -"IAP.Common.ValidateError" = "Sorry, can't validate your purchase."; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings deleted file mode 100644 index 656449af297..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bots"; -"ChatFilter.Channels" = "Kanäle"; -"ChatFilter.Private" = "Benutzer"; -"ChatFilter.Groups" = "Gruppen"; -"ChatFilter.Unread" = "Ungelesen"; -"ChatFilter.Unmuted" = "Laut"; -"ChatFilter.Favourites" = "Favoriten"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "Tabs"; -"NiceFeatures.Tabs.ShowContacts" = "Kontakte Tab anzeigen"; -"NiceFeatures.ChatScreen.Header" = "CHAT ANZEIGE"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animierte Stickers"; -"NiceFeatures.useBackCam" = "Verwende Rückkamera (runde Videos)"; - -"NiceFeatures.Folders.Header" = "ORDNER"; -"NiceFeatures.Folders.TgFolders" = "Ordner - unten"; -"NiceFeatures.Folders.TgFolders.Notice" = "Ordnerliste im iOS-Stil"; - -"NiceFeatures.RoundVideos.Header" = "RUNDE VIDEOS"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Starte mit umgedrehter Kamera"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "In Cloud speichern"; - -/*Open Pin*/ -"Chat.OpenPin" = "Pin anzeigen"; -"ChatFilter.Admin" = "Administrator"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Konto in Benachrichtigung ausblenden"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Anstelle von «Benutzer → Konto» sehen Sie nur «Benutzer»."; -"NiceFeatures.Notifications.Fix" = "Ungewünschte Benachrichtigungen deaktivieren"; -"NiceFeatures.Notifications.FixNotice" = "Nützlich, wenn du Benachrichtigungen von stummgeschalteten Chats erhältst.\nDas Antworten von Benachrichtigungen, Kontobezeichnungen sowie Medienvorschauen werden nicht verfügbar sein."; -"NiceFeatures.Filters.Header" = "Filter (Tabs)"; -"NiceFeatures.Filters.Notice" = "Wählen Sie die Anzahl der benutzerdefinierten Tabs.\nLanges Tippen auf Tab, um den Filter zu ändern."; -"NiceFeatures.Filters.ShowBadge" = "Abzeichen anzeigen (Filter)"; -"NiceFeatures.UseClassicInfoUi" = "Klassische Chat-Info-UI verwenden"; - -/*Common*/ -"Common.ExitNow" = "Jetzt verlassen"; -"Common.Later" = "Später"; -"Common.RestartRequired" = "Neustart erforderlich!"; -"NiceFeatures.Tabs.ShowNames" = "Tabnamen anzeigen"; -"Chat.ForwardAsCopy" = "Als Kopie weiterleiten"; - -/*Folder*/ -"Folder.DefaultName" = "Ordner"; -"Folder.New" = "Neuer Ordner"; -"Folder.Created" = "Ordner erstellt"; -"Folder.AddToExisting" = "Zu vorhandenem Ordner hinzufügen"; -"Folder.Updated" = "Ordner aktualisiert"; -"Folder.Create" = "Ordner erstellen..."; -"Folder.Create.Name" = "Ordnername"; -"Folder.Create.Placeholder" = "Ordner..."; -"Folder.LimitExceeded" = "Entschuldigung, Sie können nicht mehr als 3 benutzerdefinierte Ordner erstellen.\nWeitere Ordner sind in Premium verfügbar."; -"NiceFeatures.HideNumber" = "Telefon in den Einstellungen ausblenden"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Nicht verfügbar in Nicegram wegen AppStore Richtlinien"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Bitte verwenden Sie «%1» um den Standardbrowser zu konfigurieren"; -"NiceFeatures.Browser.Header" = "URLs"; -"NiceFeatures.Browser.UseBrowser" = "Im Browser öffnen"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram öffnet Links im externen Browser anstelle von In-App. Der ausgewählte Browser muss installiert sein."; -"ChatFilter.Missed" = "Verpasst"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "Unbegrenzte Anzahl angepinnter Chats"; -"Premium.SyncPins" = "Synchronisiere angepinnte Chats"; -"Premium.SyncPins.Notice.ON" = "Wenn Sie gepinnte Chats in einem anderen Client ändern, werden sie auch in Nicegram geändert."; -"Premium.SyncPins.Notice.OFF" = "Wenn Sie gepinnte Chats in einem anderen Client ändern, werden sie nicht in Nicegram geändert."; -"Premium.Missed.Header" = "Verpasste Nachrichten"; -"Premium.Missed" = "Bei verpassten Nachrichten benachrichtigen"; -"Premium.Missed.Notice" = "Wenn Sie die App nach langer Verzögerung öffnen (Schlafen, Studieren usw.), wird Nicegram Sie über ungelesene private Nachrichten und Erwähnungen informieren."; -"Folder.DeleteAsk" = "Ordner löschen"; -"Folder.NeedPremium" = "Dieser Ordner ist nur mit Premium verfügbar. Sie können Premium erwerben oder den Ordner mit Wischen löschen."; -"Common.SupportChatUsername" = "nicegram_de"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Fragen und Antworten"; -"Common.FAQ.Intro" = "Bitte beachten Sie, dass der Nicegram Support von einem einzigen Entwickler und der Community durchgeführt wird.\n\nWerfen Sie einen Blick auf die Nicegram-FAQ: Sie enthält wichtige Tipps zur Fehlerbehebung und Antworten auf die meisten Fragen."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Einzigartige Funktionen, die du nicht ablehnen kannst!"; -"IAP.Premium.Features" = "Schnellnachrichten-Übersetzer\n\nAusgewählten Ordner beim Beenden merken"; -"IAP.Premium.Activated" = "Premium aktiviert!"; -"IAP.Common.Restore" = "Einkäufe wiederherstellen"; -"IAP.Common.CantPay" = "Leider können Sie aufgrund Ihres Geräts oder Ihres Kontos keine Einkäufe tätigen."; -"IAP.Common.ErrorFetch" = "Entschuldigung, App Store Käufe können nicht abgerufen werden."; -"IAP.Common.Congrats" = "Herzlichen Glückwunsch!"; -"IAP.Common.ValidateError" = "Es tut uns leid, kann Ihren Kauf nicht validieren."; -"IAP.Common.Connecting" = "Verbinde mit AppStore..."; -"Premium.OnetapTranslate" = "Schnellübersetzen-Schaltfläche"; -"Premium.IgnoreTranslate.Title" = "Ignorierte Sprachen"; -"Premium.IgnoreTranslate.Header" = "Die Schaltfläche Schnellnachrichten-Übersetzung wird für die unten ausgewählten Sprachen DEAKTIVIERT sein."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Tabs verwalten"; -"ManageFilters.Header" = "VERFÜGBAREN FILTER VERFÜGBAREN"; - -"Messages.Translate" = "Übersetzen"; -"Messages.UndoTranslate" = "Rückgängig übersetzen"; -"Messages.TranslateError" = "Die Übersetzung ist leider nicht verfügbar."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Anmeldedatum"; -"NGLab.RegDate.Title" = "Anmeldedatum"; -"NGLab.RegDate.Notice" = "Dies ist ein ungefähres Datum"; -"NGLab.RegDate.MenuItem" = "registriert"; -"NGLab.RegDate.FetchError" = "Leider kann das Registrierungsdatum nicht abgerufen werden."; -"NGLab.BadDeviceToken" = "[iOS 11+] Gerät kann nicht verifiziert werden."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Einstellungen & Ordner mit iCloud synchronisieren"; -"NiceFeatures.BackupSettings" = "Sicherungseinstellungen & Ordner"; -"NiceFeatures.BackupSettings.Notice" = "Erstellt Sicherungsdatei. Zum Wiederherstellen auf Datei tippen."; -"NiceFeatures.BackupSettings.Done" = "Sicherung an gespeicherte Nachrichten gesendet"; -"NiceFeatures.BackupSettings.Error" = "Fehler beim Erstellen der Sicherung"; - -"NiceFeatures.RestoreSettings.Confirm" = "Sind Sie sicher, dass Sie Ordner & Einstellungen aus der Datei wiederherstellen möchten?\n⚠️ Aktuelle Daten werden überschrieben"; -"NiceFeatures.RestoreSettings.Done" = "Ordner & Einstellungen erfolgreich wiederhergestellt"; -"NiceFeatures.RestoreSettings.Error" = "Fehler beim Wiederherstellen der Einstellungen. Die Datei könnte beschädigt sein"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Du kannst keine Nachrichten im Vorschaumodus senden."; -"Gmod.Unavailable" = "Vorschaumodus nicht verfügbar"; -"Gmod.Disable.Notice" = "Deine zuletzt gesehen Einstellung wird auf ‹‹%1›› gesetzt"; -"Gmod" = "Vorschaumodus"; -"Gmod.Enable" = "Vorschaumodus aktivieren?"; -"Gmod.Disable" = "Vorschaumodus deaktivieren?"; -"Gmod.Notice" = "Dein Onlinestatus wird über die Telegram Privatsphäre-Einstellungen vor jedem versteckt.\nDie App zeigt dir eine Warnung, solltest du einen privaten Chat betreten.\nNachrichten jeglicher Art können deinen Onlinestatus zeigen."; - -"SendWithKb" = "Mit «Enter» senden"; -"NiceFeatures.ShowGmodIcon" = "Icon für Vorschaumodus anzeigen"; -"Gmod.OpenChatQ" = "Chat öffnen?"; -"Gmod.OpenChatNotice" = "Der Chat wird als gelesen markiert, du kannst aber durch Force-Touch/halten den Chat im Vorschaumodus öffnen"; -"Gmod.OpenChatBtn" = "Ja, Chat öffnen"; -"Gmod.DisableBtn" = "Warnungen deaktivieren"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Deine Nummer wird nur in der Benutzeroberfläche versteckt. Um sie vor anderen zu verbergen, verwende bitte die Privatsphäre-Einstellungen."; -"NicegramSettings.Other.showProfileId" = "Profil-ID anzeigen"; -"NicegramSettings.Other.showRegDate" = "Registrierungsdatum anzeigen"; -"Premium.OnetapTranslate.LowPower" = "Erkennung bei geringer Leistung"; -"Premium.OnetapTranslate.LowPower.Notice" = "Die App erkennt die Sprache jeder Nachricht auf deinem Gerät. Das ist eine Aufgabe mit hoher Priorität, die sich auf die Akkulaufzeit auswirken kann."; -"Premium.rememberFolderOnExit" = "Aktuellen Ordner beim Beenden merken"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings deleted file mode 100644 index 287f1273613..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bots"; -"ChatFilter.Channels" = "Channels"; -"ChatFilter.Private" = "Users"; -"ChatFilter.Groups" = "Groups"; -"ChatFilter.Unread" = "Unread"; -"ChatFilter.Unmuted" = "Unmuted"; -"ChatFilter.Favourites" = "Favourites"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "TABS"; -"NiceFeatures.Tabs.ShowContacts" = "Show Contacts Tab"; -"NiceFeatures.ChatScreen.Header" = "CHAT SCREEN"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animated Stickers"; -"NiceFeatures.useBackCam" = "Use Rear camera (Round Videos)"; - -"NiceFeatures.Folders.Header" = "FOLDERS"; -"NiceFeatures.Folders.TgFolders" = "Folders at bottom"; -"NiceFeatures.Folders.TgFolders.Notice" = "iOS-styled folder list"; - -"NiceFeatures.RoundVideos.Header" = "ROUND VIDEOS"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Start with rear camera"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Save to Cloud"; - -/*Open Pin*/ -"Chat.OpenPin" = "Show Pin"; -"ChatFilter.Admin" = "Admin"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Hide account in notification"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Instead of «User → Account» you will see only «User». For multiple accounts only."; -"NiceFeatures.Notifications.Fix" = "Disable Unwanted Notifications"; -"NiceFeatures.Notifications.FixNotice" = "Useful if you get notifications from muted chats.\nReply from notifications, account labels, and media previews will be unavailable."; -"NiceFeatures.Filters.Header" = "FILTERS (TABS)"; -"NiceFeatures.Filters.Notice" = "Select the number of custom tabs.\nLong-tap on tab to change filter."; -"NiceFeatures.Filters.ShowBadge" = "Show Badges (Filters)"; -"NiceFeatures.UseClassicInfoUi" = "Use classic Chat Info UI"; - -/*Common*/ -"Common.ExitNow" = "Exit Now"; -"Common.Later" = "Later"; -"Common.RestartRequired" = "Restart Required!"; -"NiceFeatures.Tabs.ShowNames" = "Show Tab Names"; -"Chat.ForwardAsCopy" = "Forward As Copy"; - -/*Folder*/ -"Folder.DefaultName" = "Folder"; -"Folder.New" = "New Folder"; -"Folder.Created" = "Folder created"; -"Folder.AddToExisting" = "Add to Existing Folder"; -"Folder.Updated" = "Folder updated"; -"Folder.Create" = "Create Folder..."; -"Folder.Create.Name" = "Folder Name"; -"Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Sorry, you can create no more than 3 custom folders.\nMore folders are available in Premium."; -"NiceFeatures.HideNumber" = "Hide phone in settings"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Unavailable in Nicegram due to AppStore Guidelines"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Please, Use «%1» to configure default browser"; -"NiceFeatures.Browser.Header" = "URLs"; -"NiceFeatures.Browser.UseBrowser" = "Open links in browser"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram will open links in external browser instead of in-app. Selected browser must be installed."; -"ChatFilter.Missed" = "Missed"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "UNLIMITED PINNED CHATS"; -"Premium.SyncPins" = "Sync Pinned Chats"; -"Premium.SyncPins.Notice.ON" = "If you change pinned chats in other client, they WILL CHANGE in Nicegram."; -"Premium.SyncPins.Notice.OFF" = "If you change pinned chats in other client, they WILL NOT CHANGE in Nicegram."; -"Premium.Missed.Header" = "Missed messages"; -"Premium.Missed" = "Notify on missed messages"; -"Premium.Missed.Notice" = "When you open App after long delay (sleeping, studying, etc.), Nicegram will notify you about unread private messages and mentions."; -"Folder.DeleteAsk" = "Delete Folder"; -"Folder.NeedPremium" = "This Folder is only available with Premium. You can get Premium or delete Folder with swipe."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Please note that Nicegram Support is done by the only developer and community.\n\nFirstly, take a look at the Nicegram FAQ: it has important troubleshooting tips and answers to most questions."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Unique features you can't refuse!"; -"IAP.Premium.Features" = "Quick message Translator\n\nRemember selected folder on exit"; -"IAP.Premium.Activated" = "Premium Activated!"; -"IAP.Common.Restore" = "Restore Purchases"; -"IAP.Common.CantPay" = "Sorry, but you can't make purchases because of your device or account restrictions."; -"IAP.Common.ErrorFetch" = "Sorry, сan't fetch App Store purchases."; -"IAP.Common.Congrats" = "Congratulations!"; -"IAP.Common.ValidateError" = "Sorry, can't validate your purchase."; -"IAP.Common.Connecting" = "Connecting to AppStore..."; -"Premium.OnetapTranslate" = "Quick Translate button"; -"Premium.IgnoreTranslate.Title" = "Ignored Languages"; -"Premium.IgnoreTranslate.Header" = "Quick Translate button will be DISABLED for languages you choose below."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Manage Tabs"; -"ManageFilters.Header" = "CONFIGURE AVAILABLE FILTERS"; - -"Messages.Translate" = "Translate"; -"Messages.UndoTranslate" = "Undo Translate"; -"Messages.TranslateError" = "Sorry, translation unavailable."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Get Registration Date"; -"NGLab.RegDate.Title" = "Registration Date"; -"NGLab.RegDate.Notice" = "This is an approximate date"; -"NGLab.RegDate.MenuItem" = "registered"; -"NGLab.RegDate.FetchError" = "Sorry, can't get registration date."; -"NGLab.BadDeviceToken" = "[iOS 11+] Unable to verify device."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Sync Settings & Folders with iCloud"; -"NiceFeatures.BackupSettings" = "Backup Settings & Folders"; -"NiceFeatures.BackupSettings.Notice" = "Creates backup file. Tap on file to restore."; -"NiceFeatures.BackupSettings.Done" = "Backup sent to Saved Messages"; -"NiceFeatures.BackupSettings.Error" = "Error creating backup"; - -"NiceFeatures.RestoreSettings.Confirm" = "Are you sure you want to restore Folders & Settings from file?\n⚠️ It will override current data"; -"NiceFeatures.RestoreSettings.Done" = "Folders & Settings succesfully restored"; -"NiceFeatures.RestoreSettings.Error" = "Error restoring settings. File may be corrupted"; - -/*Preview Mode*/ -"Gmod.Restricted" = "You can't send messages in Preview Mode."; -"Gmod.Unavailable" = "Preview Mode Unavailable"; -"Gmod.Disable.Notice" = "Your last seen visibility will be set to «%1»"; -"Gmod" = "Preview Mode"; -"Gmod.Enable" = "Enable Preview Mode?"; -"Gmod.Disable" = "Disable Preview Mode?"; -"Gmod.Notice" = "Your online status will be hidden from everyone by Telegram privacy settings.\nApp will show a warning if you're entering a private chat.\nYour online status may be revealed if you send or type ANY message."; - -"SendWithKb" = "Send with «Enter» button"; -"NiceFeatures.ShowGmodIcon" = "Show Preview Mode icon"; -"Gmod.OpenChatQ" = "Open chat?"; -"Gmod.OpenChatNotice" = "Chat will be marked as \"read\", but you can preview chat with a long-tap (force-touch)"; -"Gmod.OpenChatBtn" = "Yes, open the chat"; -"Gmod.DisableBtn" = "Disable warnings"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Your number will be hidden in the UI only. To hide it from others, please use Privacy settings."; -"NicegramSettings.Other.showProfileId" = "Show Profile ID"; -"NicegramSettings.Other.showRegDate" = "Show Registration Date"; -"Premium.OnetapTranslate.LowPower" = "Recognition on Low Power"; -"Premium.OnetapTranslate.LowPower.Notice" = "App detects language of each message on your device. It's a high-prefomance task which can affect your battery life."; -"Premium.rememberFolderOnExit" = "Remember current folder on exit"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings deleted file mode 100644 index 982ea6cb0fd..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,146 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bots"; -"ChatFilter.Channels" = "Canales"; -"ChatFilter.Private" = "Usuarios"; -"ChatFilter.Groups" = "Grupos"; -"ChatFilter.Unread" = "No leídos"; -"ChatFilter.Unmuted" = "No silenciado"; -"ChatFilter.Favourites" = "Favoritos"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "PESTAÑAS"; -"NiceFeatures.Tabs.ShowContacts" = "Mostrar pestaña de Contactos"; -"NiceFeatures.ChatScreen.Header" = "PANTALLA DE CHAT"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Stickers animados"; -"NiceFeatures.useBackCam" = "Usar cámara trasera (Videomensajes)"; - -"NiceFeatures.Folders.Header" = "CARPETAS"; -"NiceFeatures.Folders.TgFolders" = "Carpetas al fondo"; -"NiceFeatures.Folders.TgFolders.Notice" = "Lista de carpetas en el estilo de iOS"; - -"NiceFeatures.RoundVideos.Header" = "VIDEOS REDONDOS"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Comenzar con la cámara trasera"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Guardar en la nube"; - -/*Open Pin*/ -"Chat.OpenPin" = "Mostrar anclado"; -"ChatFilter.Admin" = "Administrador"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Ocultar cuenta en notificación"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Envés de «Usuario → Cuenta» solamente verás «Usuario». Solo para cuentas múltiples."; -"NiceFeatures.Notifications.Fix" = "Desactivar notificaciones no deseadas"; -"NiceFeatures.Notifications.FixNotice" = "Útil si recibes notificaciones de chats silenciados.\nResponder desde la notificación, etiquetas de cuenta y vistas previas de multimedia no estarán disponibles."; -"NiceFeatures.Filters.Header" = "FILTROS (PESTAÑAS)"; -"NiceFeatures.Filters.Notice" = "Selecciona el número de pestañas personalizadas. \nMantén pulsado una pestaña para cambiar el filtro."; -"NiceFeatures.Filters.ShowBadge" = "Mostrar globos (Filtros)"; -"NiceFeatures.UseClassicInfoUi" = "Usar interfaz clásica de perfil"; - -/*Common*/ -"Common.ExitNow" = "Salir ahora"; -"Common.Later" = "Después"; -"Common.RestartRequired" = "¡Reinicio requerido!"; -"NiceFeatures.Tabs.ShowNames" = "Mostrar nombres de pestañas"; -"Chat.ForwardAsCopy" = "Reenviar como copia"; - -/*Folder*/ -"Folder.DefaultName" = "Carpeta"; -"Folder.New" = "Nueva carpeta"; -"Folder.Created" = "Carpeta creada"; -"Folder.AddToExisting" = "Añadir a carpeta existente"; -"Folder.Updated" = "Carpeta actualizada"; -"Folder.Create" = "Crear carpeta…"; -"Folder.Create.Name" = "Nombre de la carpeta"; -"Folder.Create.Placeholder" = "Carpeta…"; -"Folder.LimitExceeded" = "Lo siento pero no se puede crear más que 3 archivos personalizados.\nHay más archivos disponibles en Premium."; -"NiceFeatures.HideNumber" = "Ocultar número en Ajustes"; - -/*NGWeb*/ -"NGWeb.Blocked" = "No disponible en Nicegram debido a las normas del App Store"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Ve a “%1” para configurar el navegador por omisión"; -"NiceFeatures.Browser.Header" = "URLs"; -"NiceFeatures.Browser.UseBrowser" = "Abrir enlaces en el navegador"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram abrirá los enlaces en un navegador externo en lugar del interno. El navegador seleccionado debe estar instalado."; -"ChatFilter.Missed" = "Perdido"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "CHATS ANCLADOS ILIMITADOS"; -"Premium.SyncPins" = "Sincronizar chats anclados"; -"Premium.SyncPins.Notice.ON" = "Si cambias chats anclados en otro cliente, SE CAMBIARÁN en Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Si cambias chats anclados en otro cliente, NO SE CAMBIARÁN en Nicegram."; -"Premium.Missed.Header" = "Mensajes perdidos"; -"Premium.Missed" = "Notificación de mensajes perdidos"; -"Premium.Missed.Notice" = "Cuando abres la aplicación después de un período largo (durmiendo, estudiando, etc), Nicegram te notificará sobre mensajes privados y menciones perdidos."; -"Folder.DeleteAsk" = "Eliminar carpeta"; -"Folder.NeedPremium" = "Esta carpeta solo está disponible con Premium. Puedes comprar Premium o eliminar la carpeta con el gesto de deslizar."; -"Common.SupportChatUsername" = "nicegram_es"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Preguntas frecuentes de Nicegram"; -"Common.FAQ.Intro" = "Ten en cuenta que el soporte de Nicegram sólo lo realiza un desarrollador y una comunidad.\n\nPrimero, echa un vistazo a las preguntas frecuentes de Nicegram: Tiene consejos importantes para la solución de la mayoría de problemas."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "¡Características únicas que no puedes rechazar!"; -"IAP.Premium.Activated" = "¡Premium activado!"; -"IAP.Common.Restore" = "Restaurar compras"; -"IAP.Common.CantPay" = "Lo sentimos, pero no se puede realizar compras debido a tu dispositivo o las restricciones de tu cuenta."; -"IAP.Common.ErrorFetch" = "Lo sentimos, no se pueden recuperar las compras del App Store."; -"IAP.Common.Congrats" = "¡Felicidades!"; -"IAP.Common.ValidateError" = "Lo sentimos, no se puede validar tu compra."; -"IAP.Common.Connecting" = "Conectando al App Store..."; -"Premium.OnetapTranslate" = "Botón de traducción rápida"; -"Premium.IgnoreTranslate.Title" = "Idiomas ignorados"; -"Premium.IgnoreTranslate.Header" = "El botón de traducción rápida será DESACTIVADO para los idiomas que elijas debajo."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Administrar pestañas"; -"ManageFilters.Header" = "CONFIGURAR FILTROS DISPONIBLES"; - -"Messages.Translate" = "Traducir"; -"Messages.UndoTranslate" = "Deshacer traducción"; -"Messages.TranslateError" = "Lo sentimos, la traducción no está disponible."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Obtener fecha de registro"; -"NGLab.RegDate.Title" = "Fecha de registro"; -"NGLab.RegDate.Notice" = "Esta es una fecha aproximada"; -"NGLab.RegDate.MenuItem" = "registrado"; -"NGLab.RegDate.FetchError" = "No se pudo obtener la fecha de registro."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Sincronizar ajustes y carpetas con iCloud"; -"NiceFeatures.BackupSettings" = "Copia de seguridad de ajustes y carpetas"; -"NiceFeatures.BackupSettings.Notice" = "Crea un archivo de copia de seguridad. Pulsa en el archivo para restaurarlo."; -"NiceFeatures.BackupSettings.Done" = "Copia de seguridad hecha en Mensajes guardados"; -"NiceFeatures.BackupSettings.Error" = "Error al crear la copia de seguridad"; - -"NiceFeatures.RestoreSettings.Confirm" = "¿Quieres restaurar las carpetas y ajustes desde un archivo?\n⚠️ Sobreescribirá los datos actuales"; -"NiceFeatures.RestoreSettings.Done" = "Carpetas y ajustes restaurados con éxito"; -"NiceFeatures.RestoreSettings.Error" = "Error al restaurar los ajustes. El archivo podría estar corrupto"; - -/*Preview Mode*/ -"Gmod.Restricted" = "No puedes enviar mensajes en modo vista previa."; -"Gmod.Unavailable" = "Modo vista previa no disponible"; -"Gmod.Disable.Notice" = "La visibilidad de tu última vez cambiará a “%1”"; -"Gmod" = "Modo de vista previa"; -"Gmod.Enable" = "¿Activar modo vista previa?"; -"Gmod.Disable" = "¿Desactivar modo vista previa?"; -"Gmod.Notice" = "Tu estado en línea estará oculto para todos por la configuración de privacidad de Telegram.\nLa aplicación mostrará una advertencia si estás entrando en un chat privado.\nTu estado en línea puede ser revelado si envías o escribes CUALQUIER mensaje."; - -"SendWithKb" = "Envía con el botón «Intro»"; -"NiceFeatures.ShowGmodIcon" = "Mostrar icono de modo vista previa"; -"Gmod.OpenChatQ" = "¿Abrir chat?"; -"Gmod.OpenChatNotice" = "El chat se marcará como “leído”, pero puedes obtener una vista previa del chat con un toque prolongado (3D Touch)"; -"Gmod.OpenChatBtn" = "Sí, abre el chat"; -"Gmod.DisableBtn" = "Deshabilitar advertencias"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Su número estará oculto solo en la interfaz de usuario. Para ocultarlo a los demás, por favor utilice la configuración de privacidad."; -"NicegramSettings.Other.showProfileId" = "Mostrar ID del perfil"; -"NicegramSettings.Other.showRegDate" = "Mostrar fecha de registro"; -"Premium.OnetapTranslate.LowPower" = "Reconocimiento mientras de baja potencia"; -"Premium.OnetapTranslate.LowPower.Notice" = "La aplicación detecta el idioma de cada mensaje en su aparato. Es una tarea de alta predeterminación que puede afectar la duración de su batería."; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/fa.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/fa.lproj/NiceLocalizable.strings deleted file mode 100644 index f952228c147..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/fa.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,132 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "ربات ها"; -"ChatFilter.Channels" = "کانال‌ ها"; -"ChatFilter.Private" = "کاربران"; -"ChatFilter.Groups" = "گروه ها"; -"ChatFilter.Unread" = "خوانده نشده"; -"ChatFilter.Unmuted" = "بیصدا نشده"; -"ChatFilter.Favourites" = "علاقه‌ مندی‌ها"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "زبانه ها"; -"NiceFeatures.Tabs.ShowContacts" = "نمایش برگه مخاطبین"; -"NiceFeatures.ChatScreen.Header" = "صفحه چت"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "استیکرهای انیمیشنی"; -"NiceFeatures.useBackCam" = "از دوربین عقب استفاده کنید (فیلم های گرد)"; - -"NiceFeatures.Folders.Header" = "پوشه ها"; - -"NiceFeatures.RoundVideos.Header" = "فیلم های round"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "ذخیره در فضای ابری"; - -/*Open Pin*/ -"Chat.OpenPin" = "نشان دادن سنجاق"; -"ChatFilter.Admin" = "مدیر"; -"NiceFeatures.Notifications.HideNotifyAccount" = "پنهان کردن اکانت در اطلاع"; -"NiceFeatures.Notifications.Fix" = "غیرفعال کردن اعلان های ناخواسته"; -"NiceFeatures.Notifications.FixNotice" = "در صورت دریافت اعلان از گپ های بیصدا مفید است.\nپاسخ از اعلان ها، برچسب های حساب و پیش نمایش رسانه ها در دسترس نخواهد بود."; -"NiceFeatures.Filters.Header" = "فیلترها (برگه ها)"; -"NiceFeatures.Filters.Notice" = "تعداد برگه ها را انتخاب کنید.\nبرای تغییر فیلتر نوار را حرکت دهید."; -"NiceFeatures.UseClassicInfoUi" = "استفاده از رابط کاربری کلاسیک چت"; - -/*Common*/ -"Common.ExitNow" = "خروج"; -"Common.Later" = "بعدا"; -"Common.RestartRequired" = "احتیاج به راه اندازی مجدد!"; -"NiceFeatures.Tabs.ShowNames" = "نشان دادن برگه اسم ها"; -"Chat.ForwardAsCopy" = "فروارد بدون نقل قول"; - -/*Folder*/ -"Folder.DefaultName" = "پوشه"; -"Folder.New" = "پوشه جدید"; -"Folder.Created" = "پوشه ساخته شد"; -"Folder.AddToExisting" = "به پوشه موجود اضافه شود"; -"Folder.Updated" = "بروزرسانی پوشه"; -"Folder.Create" = "ساخت پوشه"; -"Folder.Create.Name" = "نام پوشه"; -"Folder.Create.Placeholder" = "پوشه..."; -"Folder.LimitExceeded" = "متاسفم،شما بیش از ۳ پوشه نمی‌توانید ایجاد کنید.\nپوشه های بیشتر بزودی دردسترس خواهند بود."; -"NiceFeatures.HideNumber" = "پنهان کردن شماره موبایل در تنظیمات"; - -/*NGWeb*/ -"NGWeb.Blocked" = "به دلیل دستورالعمل های اپ استور ، انجام عملیات در نایس گرام مقدور نیست"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "لطفاً برای تنظیم مرورگر پیش فرض از «%1» استفاده کنید"; -"NiceFeatures.Browser.Header" = "نشانی های اینترنتی"; -"NiceFeatures.Browser.UseBrowser" = "بازکردن لینک ها در مرورگر"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram بجای خود برنامه، لینک هارا در مرورگر خارجی باز میکند. مرورگر انتخاب شده باید نصب شده باشد."; -"ChatFilter.Missed" = "از دست رفته"; - -/*Premium*/ -"Premium.Title" = "ویژه"; -"Premium.UnlimitedPins.Header" = "گفتگو های سنجاق شده نامحدود"; -"Premium.SyncPins" = "همگام سازی گفتگوهای سنجاق شده"; -"Premium.SyncPins.Notice.ON" = "اگر در برنامه دیگری گفتگو های سنجاق شده را تغییر دهید، آن ها در Nicegram نیز تغییر خواهند کرد."; -"Premium.SyncPins.Notice.OFF" = "اگر در برنامه دیگری گفتگو های سنجاق شده را تغییر دهید، آن ها در Nicegram نیز تغییر نخواهند کرد."; -"Premium.Missed.Header" = "پیام های از دست رفته"; -"Premium.Missed" = "اطلاع رسانی پیام های از دست رفته"; -"Premium.Missed.Notice" = "هنگامی که برنامه را بعد از تأخیر طولانی (خواب ، مطالعه و غیره) باز می کنید، Nicegram در مورد پیام های خصوصی خوانده نشده و منشن شده به شما اطلاع می دهد."; -"Folder.DeleteAsk" = "حذف پوشه"; -"Folder.NeedPremium" = "دسترسی به این پوشه فقط با عضویت رسمی حقوقمند مقدور می باشد شما میتوانید برای دریافت این حق عضویت اقدام و یا پوشه را با جاروب نمودن ، پاک نمایید."; -"Common.SupportChatUsername" = "nicegram_fa"; -"Common.FAQUrl" = "https://nicegram.app/faq"; -"Common.FAQ.Button" = "سوالات رایج تلگرام"; -"Common.FAQ.Intro" = "لطفاً توجه داشته باشید که پشتیبانی Nicegram توسط تنها یک توسعه دهنده و انجمن انجام می شود.\n\nدر مرحله اول، به سؤالات متداول Nicegram نگاهی بیندازید: دارای نکات مهم عیب یابی و پاسخ بیشتر سوالات است."; -"IAP.Premium.Title" = "ویژه"; -"IAP.Premium.Subtitle" = "ویژگی های منحصر به فردی که شما نمیتوانید رد کنید!"; -"IAP.Premium.Activated" = "نسخه ویژه فعال شد!"; -"IAP.Common.Restore" = "بازیابی خریدها"; -"IAP.Common.CantPay" = "متأسفیم، اما به دلیل محدودیت دستگاه یا حساب خود نمی توانید خرید کنید."; -"IAP.Common.ErrorFetch" = "متاسفانه خرید از اپ استور مقدور نیست."; -"IAP.Common.Congrats" = "تبریک میگوییم!"; -"IAP.Common.ValidateError" = "متأسفیم، نمی توانید خرید خود را تأیید کنید."; -"Premium.OnetapTranslate" = "دکمه ترجمه سریع"; -"Premium.IgnoreTranslate.Title" = "زبان های نادیده گرفته شده "; - -/*Manage Filters*/ -"ManageFilters.Title" = "مدیریت زبانه ها"; -"ManageFilters.Header" = "فیلترهای موجود را پیکربندی کنید"; - -"Messages.Translate" = "ترجمه"; -"Messages.UndoTranslate" = "بازگردانی ترجمه"; -"Messages.TranslateError" = "متأسفیم، ترجمه در دسترس نیست."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "دریافت تاریخ ثبت نام"; -"NGLab.RegDate.Title" = "تاریخ ثبت‌نام"; -"NGLab.RegDate.Notice" = "تاریخ به صورت تقریبی درج می شود"; -"NGLab.RegDate.MenuItem" = "ثبت شد"; -"NGLab.RegDate.FetchError" = "متاسفانه دریافت تاریخ ثبت نام مقدور نیست."; -"NiceFeatures.BackupSettings" = "تنظیمات پشتیبان گیری و پوشه ها"; -"NiceFeatures.BackupSettings.Done" = "تهیه نسخه پشتیبان از پیام های ذخیره شده کامل شد"; -"NiceFeatures.BackupSettings.Error" = "خطا در ایجاد نسخه پشتیبان"; - -"NiceFeatures.RestoreSettings.Confirm" = "آیا مطمئن هستید که می خواید فولدر ها و تنظیمات را از فایل بازگردانید؟\n⚠️ اطلاعات فعلی شما جایگزین خواهد شد"; - -/*Preview Mode*/ -"Gmod.Restricted" = "در حالت پیش نمایش نمی توانید پیام ارسال کنید."; -"Gmod.Unavailable" = "حالت پیش نمایش در دسترس نیست"; -"Gmod" = "حالت پیش نمایش"; -"Gmod.Enable" = "حالت پیش نمایش فعال شود؟"; -"Gmod.Disable" = "حالت پیش نمایش غیر فعال شود؟"; - -"SendWithKb" = "با دکمه enter بفرست"; -"NiceFeatures.ShowGmodIcon" = "آیکون پیش نمایش را نشان بده"; -"Gmod.OpenChatQ" = "چت را باز کنم؟"; -"Gmod.OpenChatNotice" = "چت به صورت خوانده شده نشان داده می شود ، ولی شما می توانید پیش نمایش چت را با نگه داشتن طولانی ببیند(لمس طولانی)"; -"Gmod.OpenChatBtn" = "بله ، چت را باز کن"; -"Gmod.DisableBtn" = "هشدار ها را غیر فعال کن"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "شماره شما فقط در رابط کاربری پنهان خواهد شد. برای پنهان کردن آن از دید دیگران ، لطفاً از تنظیمات حریم خصوصی استفاده کنید."; -"NicegramSettings.Other.showProfileId" = "نمایش ایدی پروفایل"; -"NicegramSettings.Other.showRegDate" = "دریافت تاریخ ثبت نام"; -"Premium.OnetapTranslate.LowPower" = "شناخت در مورد قدرت کم"; -"Premium.OnetapTranslate.LowPower.Notice" = "برنامه زبان هر پیام را در دستگاه شما تشخیص می دهد. این یک کار با عملکرد بالا است که می تواند بر عمر باتری شما تأثیر بگذارد."; -"Premium.rememberFolderOnExit" = "پوشه فعلی را در هنگام خروج بخاطر بسپارید"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/hi.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/hi.lproj/NiceLocalizable.strings deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings deleted file mode 100644 index 96f57f27dac..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bot"; -"ChatFilter.Channels" = "Canali"; -"ChatFilter.Private" = "Utenti"; -"ChatFilter.Groups" = "Gruppi"; -"ChatFilter.Unread" = "Non letti"; -"ChatFilter.Unmuted" = "Non silenziate"; -"ChatFilter.Favourites" = "Preferiti"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "TAB"; -"NiceFeatures.Tabs.ShowContacts" = "Mostra tab contatti"; -"NiceFeatures.ChatScreen.Header" = "SCHERMATA DELLE CHAT"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Adesivi animati"; -"NiceFeatures.useBackCam" = "Usa camera posteriore (Videomessaggi)"; - -"NiceFeatures.Folders.Header" = "CARTELLE"; -"NiceFeatures.Folders.TgFolders" = "Cartelle in basso"; -"NiceFeatures.Folders.TgFolders.Notice" = "lista cartelle stile iOS"; - -"NiceFeatures.RoundVideos.Header" = "Videomessaggi"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Inizia con la camera posteriore"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Salva nel cloud"; - -/*Open Pin*/ -"Chat.OpenPin" = "Mostra pin"; -"ChatFilter.Admin" = "Amministratore"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Nascondi account nella notifica"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Invece di «Utente → Account» vedrai solo «Utente». Solo per account multipli."; -"NiceFeatures.Notifications.Fix" = "Disabilita notifiche indesiderate"; -"NiceFeatures.Notifications.FixNotice" = "Utile se ricevi notifiche da chat silenziate.\nLe risposte alle notifiche, etichette degli account e le anteprime multimediali non saranno disponibili."; -"NiceFeatures.Filters.Header" = "FILTRI (TAB)"; -"NiceFeatures.Filters.Notice" = "Seleziona il numero di tab personalizzate.\nTocca a lungo la tab per cambiare filtro."; -"NiceFeatures.Filters.ShowBadge" = "Mostra badge (Filtri)"; -"NiceFeatures.UseClassicInfoUi" = "Usa l'interfaccia delle info classica"; - -/*Common*/ -"Common.ExitNow" = "Esci ora"; -"Common.Later" = "Dopo"; -"Common.RestartRequired" = "Riavvio richiesto!"; -"NiceFeatures.Tabs.ShowNames" = "Mostra nomi tab"; -"Chat.ForwardAsCopy" = "Inoltra senza firma"; - -/*Folder*/ -"Folder.DefaultName" = "Cartella"; -"Folder.New" = "Nuova cartella"; -"Folder.Created" = "Cartella creata"; -"Folder.AddToExisting" = "Aggiungi ad una cartella esistente"; -"Folder.Updated" = "Cartella aggiornata"; -"Folder.Create" = "Crea cartella..."; -"Folder.Create.Name" = "Nome cartella"; -"Folder.Create.Placeholder" = "Cartella..."; -"Folder.LimitExceeded" = "Spiacente, puoi creare solo 3 cartelle personalizzate.\nPuoi aggiungerne di più con Premium."; -"NiceFeatures.HideNumber" = "Nascondi il numero di telefono nelle impostazioni"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Non disponibile su Nicegram a causa delle linee guida dell'App Store"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Per favore, usa «%1» per configurare il tuo browser predefinito"; -"NiceFeatures.Browser.Header" = "URL"; -"NiceFeatures.Browser.UseBrowser" = "Apri link nel browser"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram aprirà i link in un browser esterno invece di farlo nell'app.\nIl browser selezionato deve essere installato."; -"ChatFilter.Missed" = "Mancati"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "CHAT FISSATE ILLIMITATE"; -"Premium.SyncPins" = "Sincronizza chat fissate"; -"Premium.SyncPins.Notice.ON" = "Se cambi chat fissate in un altro client, esse CAMBIERANNO su Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Se cambi chat fissate in un altro client, esse NON CAMBIERANNO su Nicegram."; -"Premium.Missed.Header" = "Messaggi mancati"; -"Premium.Missed" = "Notifica per i messaggi mancati"; -"Premium.Missed.Notice" = "Quando apri l'app dopo un po' di tempo (dormita, studio, ecc.), Nicegram ti notificherà delle chat private e menzioni non lette."; -"Folder.DeleteAsk" = "Cancella cartella"; -"Folder.NeedPremium" = "Questa cartella è disponibile solo con Premium. Ottieni Premium o dovrai cancellare la cartella con uno swipe."; -"Common.SupportChatUsername" = "nicegram_it"; -"Common.FAQUrl" = "https://nicegram.app/it/faq/"; -"Common.FAQ.Button" = "FAQ Nicegram"; -"Common.FAQ.Intro" = "Per favore, nota che il supporto di Nicegram è operato solo dall'unico sviluppatore e dalla community.\n\nPrima di tutto, dai una occhiata alle FAQ di Nicegram: contiene molte soluzioni a problemi frequenti."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Feature uniche che non puoi rifiutare!"; -"IAP.Premium.Features" = "Traduttore istantaneo dei messaggi\n\nRicorda la cartella corrente all'uscita"; -"IAP.Premium.Activated" = "Premium attivato!"; -"IAP.Common.Restore" = "Ripristina acquisti"; -"IAP.Common.CantPay" = "Spiacente, non puoi effettuare acquisti a causa di restrizioni sul tuo dispositivo o account."; -"IAP.Common.ErrorFetch" = "Spiacente, impossibile ottenere i tuoi acquisti dall'App Store."; -"IAP.Common.Congrats" = "Congratulazioni!"; -"IAP.Common.ValidateError" = "Spiacente, non è stato possibile convalidare l'acquisto."; -"IAP.Common.Connecting" = "Connessione all'AppStore..."; -"Premium.OnetapTranslate" = "Bottone di traduzione rapida"; -"Premium.IgnoreTranslate.Title" = "Lingue ignorate"; -"Premium.IgnoreTranslate.Header" = "Il bottone di traduzione rapida sarà DISABILITATO per le lingue che selezioni sotto."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Gestisci tab"; -"ManageFilters.Header" = "CONFIGURA FILTRI DISPONIBILI"; - -"Messages.Translate" = "Traduci"; -"Messages.UndoTranslate" = "Annulla traduzione"; -"Messages.TranslateError" = "Spiacente, traduzione non disponibile."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Ottieni data registrazione"; -"NGLab.RegDate.Title" = "Data registrazione"; -"NGLab.RegDate.Notice" = "Questa è una data approssimativa"; -"NGLab.RegDate.MenuItem" = "registrato"; -"NGLab.RegDate.FetchError" = "Spiacente, impossibile ottenere data registrazione."; -"NGLab.BadDeviceToken" = "[iOS 11+] Impossibile verificare il dispositivo."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Sincronizza impostazioni e cartelle con iCloud"; -"NiceFeatures.BackupSettings" = "Backup impostazioni e cartelle"; -"NiceFeatures.BackupSettings.Notice" = "Crea file di backup. Tocca il file per ripristinare."; -"NiceFeatures.BackupSettings.Done" = "Backup eseguito nei messaggi salvati"; -"NiceFeatures.BackupSettings.Error" = "Errore creazione backup"; - -"NiceFeatures.RestoreSettings.Confirm" = "Sicuro di voler ripristinare cartelle e impostazioni dal file?\n⚠️ Questo sovrascriverà i dati attuali"; -"NiceFeatures.RestoreSettings.Done" = "✔️ Cartelle e impostazioni ripristinate"; -"NiceFeatures.RestoreSettings.Error" = "Errore ripristino impostazioni. Il file potrebbe essere corrotto"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Non puoi inviare messaggi nella modalità anteprima."; -"Gmod.Unavailable" = "Modalità anteprima non disponibile"; -"Gmod.Disable.Notice" = "La tua visibilità sarà impostata su «%1»"; -"Gmod" = "Modalità anteprima"; -"Gmod.Enable" = "Abilitare modalità anteprima?"; -"Gmod.Disable" = "Disabilitare modalità anteprima?"; -"Gmod.Notice" = "Il tuo stato online sarà nascosto da tutti tramite le impostazioni della privacy di Telegram.\nL'app mostrerà un avviso se entri in una chat privata.\nIl tuo stato online verrà rivelato se mandi o scrivi qualunque messaggio."; - -"SendWithKb" = "Invia con il pulsante «Invio»"; -"NiceFeatures.ShowGmodIcon" = "Mostra icona modalità anteprima"; -"Gmod.OpenChatQ" = "Aprire chat?"; -"Gmod.OpenChatNotice" = "La chat verrà segnata come \"letta\", ma puoi fare l'anteprima della chat con un tocco lungo (force-touch)"; -"Gmod.OpenChatBtn" = "Sì, apri la chat"; -"Gmod.DisableBtn" = "Disabilita avvisi"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Il tuo numero verrà nascosto solo nell'interfaccia. Per nasconderlo dagli altri, apri le impostazioni della Privacy."; -"NicegramSettings.Other.showProfileId" = "Mostra l'ID del profilo"; -"NicegramSettings.Other.showRegDate" = "Mostra la data di registrazione"; -"Premium.OnetapTranslate.LowPower" = "Riconoscimento a bassa potenza"; -"Premium.OnetapTranslate.LowPower.Notice" = "L'app rileva la lingua di ogni messaggio sul dispositivo. Si tratta di un'operazione ad alto consumo che può influire sulla durata della batteria."; -"Premium.rememberFolderOnExit" = "Ricorda la cartella corrente all'uscita"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings deleted file mode 100644 index a2d584ea569..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "봇"; -"ChatFilter.Channels" = "채널"; -"ChatFilter.Private" = "사용자"; -"ChatFilter.Groups" = "그룹"; -"ChatFilter.Unread" = "읽지 않음"; -"ChatFilter.Unmuted" = "음소거 해제"; -"ChatFilter.Favourites" = "즐겨찾기"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "탭"; -"NiceFeatures.Tabs.ShowContacts" = "연락처 탭 보이기"; -"NiceFeatures.ChatScreen.Header" = "채팅 화면"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "애니메이션 스티커"; -"NiceFeatures.useBackCam" = "후면 카메라 사용 (라운드 비디오)"; - -"NiceFeatures.Folders.Header" = "폴더"; -"NiceFeatures.Folders.TgFolders" = "폴더를 하단에 표시"; -"NiceFeatures.Folders.TgFolders.Notice" = "iOS 스타일 폴더 목록"; - -"NiceFeatures.RoundVideos.Header" = "라운드 비디오"; -"NiceFeatures.RoundVideos.UseRearCamera" = "후면 카메라로 시작"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "클라우드에 저장"; - -/*Open Pin*/ -"Chat.OpenPin" = "핀 표시"; -"ChatFilter.Admin" = "관리자"; -"NiceFeatures.Notifications.HideNotifyAccount" = "알림에서 계정 숨기기"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "«사용자→계정»대신 «사용자»만 표시됩니다. 멀티계정에만 해당됩니다."; -"NiceFeatures.Notifications.Fix" = "원치 않는 알림 비활성화"; -"NiceFeatures.Notifications.FixNotice" = "음소거 된 채팅에서 알림을 받는 경우 유용합니다.\n알림, 계정 라벨 및 미디어 미리보기에서는 답장을 보낼 수 없습니다."; -"NiceFeatures.Filters.Header" = "필터 (탭)"; -"NiceFeatures.Filters.Notice" = "사용자 정의 탭 수를 선택하세요.\n필터를 변경하려면 탭을 길게 누르세요."; -"NiceFeatures.Filters.ShowBadge" = "배지 표시 (필터)"; -"NiceFeatures.UseClassicInfoUi" = "기본 채팅 정보 UI 사용"; - -/*Common*/ -"Common.ExitNow" = "지금 종료"; -"Common.Later" = "나중에"; -"Common.RestartRequired" = "다시 시작 해야합니다!"; -"NiceFeatures.Tabs.ShowNames" = "탭 이름 표시"; -"Chat.ForwardAsCopy" = "복사본으로 전달"; - -/*Folder*/ -"Folder.DefaultName" = "폴더"; -"Folder.New" = "새 폴더"; -"Folder.Created" = "폴더가 추가 되었습니다"; -"Folder.AddToExisting" = "기존 폴더에 추가"; -"Folder.Updated" = "폴더가 업데이트 되었습니다"; -"Folder.Create" = "폴더 만들기"; -"Folder.Create.Name" = "폴더 이름"; -"Folder.Create.Placeholder" = "폴더..."; -"Folder.LimitExceeded" = "죄송합니다. 사용자 지정 폴더는 3 개까지만 만들 수 있습니다.\n프리미엄 에서 더 많은 폴더를 사용할 수 있습니다."; -"NiceFeatures.HideNumber" = "설정에서 전화번호 숨기기"; - -/*NGWeb*/ -"NGWeb.Blocked" = "AppStore 지침으로 인해 Nicegram 에서 사용할 수 없음"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "%1 을 사용하여 기본 브라우저를 설정 하세요"; -"NiceFeatures.Browser.Header" = "웹주소"; -"NiceFeatures.Browser.UseBrowser" = "브라우저에서 링크 열기"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram은 인앱 대신 외부 브라우저에서 링크를 엽니 다. 선택한 브라우저는 설치되어 있어야 합니다."; -"ChatFilter.Missed" = "부재중"; - -/*Premium*/ -"Premium.Title" = "프리미엄"; -"Premium.UnlimitedPins.Header" = "채팅창 무제한 고정"; -"Premium.SyncPins" = "고정 된 채팅 동기화"; -"Premium.SyncPins.Notice.ON" = "다른 장치에서 고정 된 채팅을 변경하면, Nicegram 에서도 변경됩니다."; -"Premium.SyncPins.Notice.OFF" = "다른 장치에서 고정 된 채팅을 변경하여도, Nicegram 에서는 변경되지 않습니다."; -"Premium.Missed.Header" = "부재중 메세지"; -"Premium.Missed" = "부재중 메시지 알림"; -"Premium.Missed.Notice" = "장시간 지연 후 (수면, 공부 등) 앱을 열면, Nicegram 이 읽지 않은 개인 메시지 및 멘션에 대해 알려줍니다."; -"Folder.DeleteAsk" = "폴더 삭제"; -"Folder.NeedPremium" = "이 폴더는 프리미엄 에서만 사용할 수 있습니다. 옆으로 밀어서 프리미엄을 받거나 폴더를 삭제할 수 있습니다."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Nicegram 지원은 유일한 개발자 및 커뮤니티에 의해 수행됩니다.\n\n먼저 Nicegram FAQ 를 살펴보세요. 대부분의 질문에 대한 중요한 문제 해결 팁과 답변이 있습니다."; -"IAP.Premium.Title" = "프리미엄"; -"IAP.Premium.Subtitle" = "거부 할 수없는 독특한 기능!"; -"IAP.Premium.Features" = "빠른 메세지 번역기\n\n종료시 선택한 폴더 기억하기"; -"IAP.Premium.Activated" = "프리미엄 활성화!"; -"IAP.Common.Restore" = "구입 내역 복원"; -"IAP.Common.CantPay" = "죄송합니다. 기기 또는 계정 제한으로 인해 구매할 수 없습니다."; -"IAP.Common.ErrorFetch" = "죄송합니다. AppStore 구매를 가져올 수 없습니다."; -"IAP.Common.Congrats" = "축하합니다!"; -"IAP.Common.ValidateError" = "죄송합니다. 구매를 확인할 수 없습니다."; -"IAP.Common.Connecting" = "AppStore 에 연결 중..."; -"Premium.OnetapTranslate" = "빠른 번역 버튼"; -"Premium.IgnoreTranslate.Title" = "제외한 언어"; -"Premium.IgnoreTranslate.Header" = "아래에서 선택한 언어는 빠른 번역 버튼이 비활성화 됩니다."; - -/*Manage Filters*/ -"ManageFilters.Title" = "탭 관리"; -"ManageFilters.Header" = "사용 가능한 필터 구성"; - -"Messages.Translate" = "번역"; -"Messages.UndoTranslate" = "번역 취소"; -"Messages.TranslateError" = "죄송합니다. 번역 할 수 없습니다."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "등록 날짜 확인"; -"NGLab.RegDate.Title" = "등록 날짜"; -"NGLab.RegDate.Notice" = "이것은 대략적인 날짜입니다"; -"NGLab.RegDate.MenuItem" = "등록됨"; -"NGLab.RegDate.FetchError" = "죄송합니다. 등록일을 확인할 수 없습니다."; -"NGLab.BadDeviceToken" = "[iOS 11+] 장치를 확인할 수 없습니다."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "iCloud 에 설정 및 폴더 동기화"; -"NiceFeatures.BackupSettings" = "백업 설정 및 폴더"; -"NiceFeatures.BackupSettings.Notice" = "백업 파일을 생성합니다. 복원 할 파일을 탭하세요."; -"NiceFeatures.BackupSettings.Done" = "저장된 메시지로 백업 전송"; -"NiceFeatures.BackupSettings.Error" = "백업 생성 오류"; - -"NiceFeatures.RestoreSettings.Confirm" = "파일에서 폴더 및 설정을 복원 하시겠습니까?\n⚠️ 현재 데이터를 덮어 씌웁니다"; -"NiceFeatures.RestoreSettings.Done" = "폴더 및 설정이 성공적으로 복원되었습니다"; -"NiceFeatures.RestoreSettings.Error" = "설정을 복원하는 중에 오류가 발생했습니다. 파일이 손상되었을 수 있습니다"; - -/*Preview Mode*/ -"Gmod.Restricted" = "미리보기 모드에서는 메시지를 보낼 수 없습니다."; -"Gmod.Unavailable" = "미리보기 모드를 사용할 수 없음"; -"Gmod.Disable.Notice" = "마지막 접속 시간이 %1 로 설정 됩니다"; -"Gmod" = "미리보기 모드"; -"Gmod.Enable" = "미리보기 모드를 사용 하시겠습니까?"; -"Gmod.Disable" = "미리보기를 사용 중지 하시겠습니까?"; -"Gmod.Notice" = "텔레그램 개인 정보 설정에 의해 온라인 상태가 모든 사람에게 표시되지 않습니다.\n비공개 채팅에 들어가면 앱에 경고가 표시됩니다.\n메시지를 보내거나 입력하면 온라인 상태가 공개 될 수 있습니다."; - -"SendWithKb" = "«입력»버튼으로 보내기"; -"NiceFeatures.ShowGmodIcon" = "미리보기 모드 아이콘 표시"; -"Gmod.OpenChatQ" = "채팅 열기"; -"Gmod.OpenChatNotice" = "채팅은 \"읽음\"으로 표시되지만 길게 탭 (강제 터치)하여 채팅을 미리 볼 수 있습니다."; -"Gmod.OpenChatBtn" = "예, 채팅을 엽니다."; -"Gmod.DisableBtn" = "경고 비활성화"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "전화 번호는 UI에서만 숨겨집니다. 다른 사람에게 숨기려면 개인 정보 설정을 사용하세요."; -"NicegramSettings.Other.showProfileId" = "프로필 ID 표시"; -"NicegramSettings.Other.showRegDate" = "가입 날짜 표시"; -"Premium.OnetapTranslate.LowPower" = "저전력 인식"; -"Premium.OnetapTranslate.LowPower.Notice" = "앱은 장치에서 각 메시지의 언어를 감지합니다. 배터리 수명에 영향을 미칠 수있는 고성능 작업입니다."; -"Premium.rememberFolderOnExit" = "종료시 현재 폴더 기억하기"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ku.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ku.lproj/NiceLocalizable.strings deleted file mode 100644 index 130dcbaff17..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ku.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,142 +0,0 @@ -"AppName" = "نایسگرام"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "بۆتەکان"; -"ChatFilter.Channels" = "کەناڵەکان"; -"ChatFilter.Private" = "بەکارهێنەرەکان"; -"ChatFilter.Groups" = "گروپەکان"; -"ChatFilter.Unread" = "نەخوێندراوەکان"; -"ChatFilter.Unmuted" = "بێدەنگنەکراوەکان"; -"ChatFilter.Favourites" = "دڵخوازەکان"; - -/*Nice Features*/ -"NiceFeatures.Title" = "نایسگرام"; -"NiceFeatures.Tabs.Header" = "تابەکان"; -"NiceFeatures.Tabs.ShowContacts" = "نیشاندانی تابی کۆنتاکتەکان"; -"NiceFeatures.ChatScreen.Header" = "ڕووکاری چات"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "لەزگە جووڵاوەکان"; -"NiceFeatures.useBackCam" = "بەکارهێنانی کامێرای پشتەوە (ڤیدیۆی بازنەیی)"; - -"NiceFeatures.Folders.Header" = "بوخچەکان"; -"NiceFeatures.Folders.TgFolders" = "بوخچەکان لە خوارەوە"; -"NiceFeatures.Folders.TgFolders.Notice" = "لیستی بوخچەی شێوازی iOS"; - -"NiceFeatures.RoundVideos.Header" = "ڤیدیۆ بازنەییەکان"; -"NiceFeatures.RoundVideos.UseRearCamera" = "دەستپێکردن بە کامێرای پشتەوە"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "هەڵگرتن لە کڵاود"; - -/*Open Pin*/ -"Chat.OpenPin" = "نیشاندانی هەڵواسراو"; -"ChatFilter.Admin" = "بەڕێوەبەر"; -"NiceFeatures.Notifications.HideNotifyAccount" = "شاردنەوەی هەژمار لە ئاگادارکردنەوە"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "لە جیاتیی «بەکارهێنەر ← هەژمار» تەنها «بەکارهێنەر» دەبینیت. تەنها بۆ چەند هەژمارێک."; -"NiceFeatures.Notifications.Fix" = "ناچالاککردنی ئاگادارکردنەوە نەویستراوەکان"; -"NiceFeatures.Notifications.FixNotice" = "سوودبەخشە ئەگەر ئاگادارکردنەوەت لە چاتە بێدەنگکراوەکانەوە بۆ بێت. وەڵامدانەوە لە ئاگادارکردنەوەکانەوە، لە باجی هەژمارەکانەوە، و پێشبینییەکانی میدیا بەردەستنابن."; -"NiceFeatures.Filters.Header" = "پاڵاوتنەکان (تابەکان)"; -"NiceFeatures.Filters.Notice" = "ژمارەیەک تابی ڕاسپێردراو هەڵبژێرە.\nبۆ ماوەیەکی درێژ دەستبنێ بە تابەکە بۆ گۆڕینی پاڵاوتن."; -"NiceFeatures.Filters.ShowBadge" = "نیشاندانی باجەکان (پاڵاونتەکان)"; -"NiceFeatures.UseClassicInfoUi" = "بەکارهێنانی زانیاریی چاتی کلاسیکی"; - -/*Common*/ -"Common.ExitNow" = "دەرچوون ئێستا"; -"Common.Later" = "دواتر"; -"Common.RestartRequired" = "دەستپێکردنەوە پێویستە!"; -"NiceFeatures.Tabs.ShowNames" = "نیشاندانی ناوی تابەکان"; -"Chat.ForwardAsCopy" = "گواستنەوە وەک لەبەرگیراوە"; - -/*Folder*/ -"Folder.DefaultName" = "بوخچە"; -"Folder.New" = "بوخچەی نوێ"; -"Folder.Created" = "بوخچە دروستکرا"; -"Folder.AddToExisting" = "زیادکردن بۆ بوخچەیەکی هەبوو"; -"Folder.Updated" = "بوخچە نوێکرایەوە"; -"Folder.Create" = "دروستکردنی بوخچە..."; -"Folder.Create.Name" = "ناوی بوخچە"; -"Folder.Create.Placeholder" = "بوخچە..."; -"Folder.LimitExceeded" = "بمبوورە، تەنها ٣ بوخچە دەتوانی بە دڵی خۆت دروستبکەیت. بوخچەی زۆرتر بە پارەیە."; -"NiceFeatures.HideNumber" = "شاردنەوەی تەلەفۆن لە ڕێکخستنەکان"; - -/*NGWeb*/ -"NGWeb.Blocked" = "بەردەست نییە لە نایسگرام لەبەر یاساکانی ئاپستۆر"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "تکایە «%1» بەکاربێنە بۆ گۆڕینی وێبگەڕی بنەڕەت"; -"NiceFeatures.Browser.Header" = "یواڕێڵەکان"; -"NiceFeatures.Browser.UseBrowser" = "کردنەوەی بەستەرەکان لە وێبگەڕ"; -"NiceFeatures.Browser.UseBrowserNotice" = "نایسگرام لینکەکان لە وێبگەڕی دەرەکییەوە دەکاتەوە لە جیاتیی ناو بەرنامە. وێبگەڕە هەڵبژێردراوەکە دەبێت هەبێت."; -"ChatFilter.Missed" = "لەدەستچوو"; - -/*Premium*/ -"Premium.Title" = "پارە"; -"Premium.UnlimitedPins.Header" = "چاتی هەڵواسراوی بێسنوور"; -"Premium.SyncPins" = "هاوکاتکردنی چاتە هەڵواسراوەکان"; -"Premium.SyncPins.Notice.ON" = "ئەگەر پەیامە هەڵواسراوەکان لە شوێنی دیکە بگۆڕیت، لە نایسکرامیش دەگۆڕێن."; -"Premium.SyncPins.Notice.OFF" = "ئەگەر پەیامە هەڵواسراوەکان لە شوێنی دیکە بگۆڕیت، لە نایسگرام ناگۆڕێن."; -"Premium.Missed.Header" = "پەیامە لەدەستچووەکان"; -"Premium.Missed" = "ئاگادارکردنەوە لە پەیامە بیرچووەکان"; -"Premium.Missed.Notice" = "کاتێک بەرنامەکە دوای ماوەیەکی زۆر دەکەیتەوە (خەوتن، سەعیکردن، هتد.)، نایسگرام ئاگادارت دەکاتەوە دەربارەی پەیامە تەیبەتییە نەخوێندراوەکان و ئاماژەپێکردنەکان."; -"Folder.DeleteAsk" = "سڕینەوەی بوخچە"; -"Folder.NeedPremium" = "ئەم بوخچەیە تەنها بە پارە هەیە. دەتوانیت بە پارە بەدەستی بهێنیت یان بە خشاندن بوخچە بسڕیتەوە."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq"; -"Common.FAQ.Button" = "پرسیارە باوەکانی نایسگرام"; -"Common.FAQ.Intro" = "تکایە تێبینیی ئەوە بکە کە پشتگیریی نایسگرام تەنها لەلایەن پەرەپێدەران و کۆمەڵگاوە دەکرێت.\n\nیەکەمجار، پرسیارە باوەکانی نایسگرام ببینە: شتی گرنگی کێشەشکاندنی هەیە و وەڵامی زۆربەی پرسیارەکان دەداتەوە."; -"IAP.Premium.Title" = "پارە"; -"IAP.Premium.Subtitle" = "تایبەتمەندیی تایبەت کە ناتوانیت ڕەتی بکەیتەوە!"; -"IAP.Premium.Activated" = "پارەدان چالاککرا!"; -"IAP.Common.Restore" = "گەڕاندنەوەی پارەدانەکان"; -"IAP.Common.CantPay" = "ببورە، بەڵام ناتوانیت شت بکڕیت لەبەر ئامێرەکەت یان سنوورەکانی هەژمارت."; -"IAP.Common.ErrorFetch" = "ببورە، نەتواندرا پارەدانەکانی ئاپستۆر بهێندرێن."; -"IAP.Common.Congrats" = "پیرۆزە!"; -"IAP.Common.ValidateError" = "ببورە، ناتوانیت پارەدانەکەت بسەلمێنییت."; -"IAP.Common.Connecting" = "دەبەسترێتەوە لەگەڵ ئاپستۆر..."; -"Premium.OnetapTranslate" = "دوگمەی وەرگێڕانی خێرا"; -"Premium.IgnoreTranslate.Title" = "زمانە پشتگوێخراوەکان"; -"Premium.IgnoreTranslate.Header" = "دوگمەی وەرگێری خێرا ناچالاک دەبێت بۆ ئەو زمانانەی لە خوارەوە هەڵیان دەبژێرێت."; - -/*Manage Filters*/ -"ManageFilters.Title" = "بەڕێوەبردنی تابەکان"; -"ManageFilters.Header" = "گۆڕینی پاڵاوتنە بەردەستەکان"; - -"Messages.Translate" = "وەرگێڕان"; -"Messages.UndoTranslate" = "گەڕاندنەوەی وەرگێڕان"; -"Messages.TranslateError" = "ببورە، وەرگێڕان بەردەست نییە."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "دەستکەوتنی بەرواری تۆمارکردن"; -"NGLab.RegDate.Title" = "بەرواری تۆمارکردن"; -"NGLab.RegDate.Notice" = "ئەمە بەروارێکی نزیککراوەیە"; -"NGLab.RegDate.MenuItem" = "تۆمارکراو"; -"NGLab.RegDate.FetchError" = "ببورە، نەتواندرا بەرواری تۆمارکردن دەستبکەوێت."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "هاوکاتکردنی ڕێکخستن و بوخچەکان لەگەڵ ئایکڵاود"; -"NiceFeatures.BackupSettings" = "باکەپکردنی ڕێکخستنەکان و بوخچەکان"; -"NiceFeatures.BackupSettings.Notice" = "فایلی باکەپ دروستدەکات. دەستبنێ بە فایل بۆ گەڕاندنەوە."; -"NiceFeatures.BackupSettings.Done" = "باکەپ نێردرا بۆ پەیامە پاشەکەوتکراوەکان"; -"NiceFeatures.BackupSettings.Error" = "هەڵە ڕوویدا لە دروستکردنی باکەپ"; - -"NiceFeatures.RestoreSettings.Confirm" = "ئایا بێگومانیت لەوەی کە دەتەوێت بوخچە و ڕێکخستنەکان بگەڕێنیتەوە لە فایلەوە؟\n⚠️ بەسەر زانیارییەکانی ئێستادا دێتەوە"; -"NiceFeatures.RestoreSettings.Done" = "بوخچە و ڕێکخستنەکان سەرکەوتووانە گەڕێندرانەوە"; -"NiceFeatures.RestoreSettings.Error" = "هەڵە ڕوویدا لە گەڕاندنەوەی ڕێکخستنەکان. فایلەکە لەوانەیە شێوابێت"; - -/*Preview Mode*/ -"Gmod.Restricted" = "ناتوانیت پەیام بنێریت لە دۆخی پێشبینین."; -"Gmod.Unavailable" = "دۆخی پێشبینین بەردەست نییە"; -"Gmod.Disable.Notice" = "کۆتا جار بینینی تۆ کرا بە «%1»"; -"Gmod" = "دۆخی پێشبینین"; -"Gmod.Enable" = "دۆخی پێشبینین چالاکبکرێت؟"; -"Gmod.Disable" = "دۆخی پێشبینین ناچالاکبکرێت؟"; -"Gmod.Notice" = "خەڵک ناتوانێت بزانێت ئەگەر لەسەرهێڵیت بە ڕێکخستنەکانی تایبەتی تێلێگرام.\nبەرنامەکە ئاگادارکردنەوەیەک نیشاندەدات ئەگەر بچیتە چاتێکی تایبەت.\nلەوانەیە دەربکەوێت کە لەسەرهێڵیت ئەگەر هەر پەیامێک بنووسیت یان بنێریت."; - -"SendWithKb" = "ناردن بە دوگمەی «Enter»"; -"NiceFeatures.ShowGmodIcon" = "نیشاندانی وینۆچکەی دۆخی پێشبینین"; -"Gmod.OpenChatQ" = "چاتەکە بکرێتەوە؟"; -"Gmod.OpenChatNotice" = "چاتەکە وەک «خوێدراو» دیاری دەکرێت، بەڵام تۆ دەتوانیت چاتە پێشببینیت بە دەستدان لێی بۆ ماوەیەکی درێژ"; -"Gmod.OpenChatBtn" = "بەڵێ، چاتەکە بکەرەوە"; -"Gmod.DisableBtn" = "ناچالاککردنی ئاگادارکردنەوەکان"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "ژمارەکەت تەنها لە ڕووکارەکە دەرناکەوێت. بۆ ئەوەی لە ئەوانەی دیکەی بشاریتەوە، تکایە ڕێکخستنەکانی پارێزراوی بەکاربێنە."; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings deleted file mode 100644 index 92f2640d4be..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bot"; -"ChatFilter.Channels" = "Kanały"; -"ChatFilter.Private" = "Użytk"; -"ChatFilter.Groups" = "Grupy"; -"ChatFilter.Unread" = "Nieprzeczyt"; -"ChatFilter.Unmuted" = "Wyłącz. pow."; -"ChatFilter.Favourites" = "Ulubione"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "ZAKŁADKI"; -"NiceFeatures.Tabs.ShowContacts" = "Pokaż zakładkę kontakty"; -"NiceFeatures.ChatScreen.Header" = "Ekran czatu"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animowane naklejki"; -"NiceFeatures.useBackCam" = "Użyj tylnej kamery (okrągłe wideo)"; - -"NiceFeatures.Folders.Header" = "FOLDERY"; -"NiceFeatures.Folders.TgFolders" = "Foldery na dole"; -"NiceFeatures.Folders.TgFolders.Notice" = "Lista folderów w stylu iOS"; - -"NiceFeatures.RoundVideos.Header" = "OKRĄGŁE WIDEO"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Uruchom z tylną kamerą"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Zapisz w chmurze"; - -/*Open Pin*/ -"Chat.OpenPin" = "Pokaż przypiętą"; -"ChatFilter.Admin" = "Administrator"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Ukryj konto w powiadomieniu"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Zamiast «Użytkownik → Konto» zobaczysz tylko «Użytkownik». Tylko dla wielu kont."; -"NiceFeatures.Notifications.Fix" = "Wyłącz niechciane powiadomienia"; -"NiceFeatures.Notifications.FixNotice" = "Przydatne, jeśli otrzymujesz powiadomienia z czatów z wyłączonymi powiadomieniami.\nOpcja „Odpowiedz” nie będzie dostępna dla powiadomień, etykiet kont i podglądów multimediów."; -"NiceFeatures.Filters.Header" = "FILTRY (ZAKŁADKI)"; -"NiceFeatures.Filters.Notice" = "Wybierz liczbę własnych kart.\nPrzytrzymaj kartę, aby zmienić filtr."; -"NiceFeatures.Filters.ShowBadge" = "Pokaż plakietki (filtry)"; -"NiceFeatures.UseClassicInfoUi" = "Użyj klasycznego wyglądu Informacji o czacie"; - -/*Common*/ -"Common.ExitNow" = "Wyjdź teraz"; -"Common.Later" = "Później"; -"Common.RestartRequired" = "Wymagane ponowne uruchomienie!"; -"NiceFeatures.Tabs.ShowNames" = "Pokaż nazwy zakładek"; -"Chat.ForwardAsCopy" = "Przekaż jako kopię"; - -/*Folder*/ -"Folder.DefaultName" = "Folder"; -"Folder.New" = "Nowy folder"; -"Folder.Created" = "Utworzono folder"; -"Folder.AddToExisting" = "Dodaj do istniejącego folderu"; -"Folder.Updated" = "Folder zaktualizowany"; -"Folder.Create" = "Utwórz folder..."; -"Folder.Create.Name" = "Nazwa folderu"; -"Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Przepraszamy, nie możesz utworzyć więcej niż 3 niestandardowe foldery.\nWięcej folderów jest dostępnych w Premium."; -"NiceFeatures.HideNumber" = "Ukryj numer telefonu w ustawieniach"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Niedostępne w Nicegram ze względu na wytyczne AppStore"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Użyj «%1», aby skonfigurować domyślną przeglądarkę"; -"NiceFeatures.Browser.Header" = "URL"; -"NiceFeatures.Browser.UseBrowser" = "Otwórz linki w przeglądarce"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram otworzy linki w zewnętrznej przeglądarce zamiast w aplikacji. Wybrana przeglądarka musi być zainstalowana."; -"ChatFilter.Missed" = "Nieodebrane"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "NIELIMITOWANA LICZBA PRZYPIĘTYCH CZATÓW"; -"Premium.SyncPins" = "Synchronizuj przypięte czaty"; -"Premium.SyncPins.Notice.ON" = "Jeśli zmienisz przypięte czaty w innym kliencie, ZMIENIĄ SIĘ one też w Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Jeśli zmienisz przypięte czaty w innym kliencie, NIE ZMIENIĄ SIĘ one w Nicegram."; -"Premium.Missed.Header" = "Nieodebrane wiadomości"; -"Premium.Missed" = "Powiadom o nieodebranych wiadomościach"; -"Premium.Missed.Notice" = "Gdy otworzysz aplikację po długim czasie (spanie, nauka itp.), Nicegram powiadomi cię o nieprzeczytanych wiadomościach prywatnych i wzmiankach."; -"Folder.DeleteAsk" = "Usuń folder"; -"Folder.NeedPremium" = "Ten folder jest dostępny tylko w wersji Premium. Możesz pobrać wersję Premium lub usunąć folder za pomocą przesunięcia."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Wsparcie Nicegram jest wykonywane przez jednego programistę i społeczność.\n\nPo pierwsze, zajrzyj do FAQ: zawiera ważne wskazówki dotyczące rozwiązywania problemów i odpowiedzi na większość pytań."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Unikalne funkcje, których nie można odmówić!"; -"IAP.Premium.Features" = "Szybkie tłumaczenie wiadomości\n\nZapamiętaj wybrany folder przy wyjściu"; -"IAP.Premium.Activated" = "Aktywowano wersję Premium!"; -"IAP.Common.Restore" = "Odtwórz zakupione"; -"IAP.Common.CantPay" = "Nie możesz dokonywać zakupów z powodu ograniczeń dotyczących twojego urządzenia lub konta."; -"IAP.Common.ErrorFetch" = "Nie mogę odebrać zakupów w App Store."; -"IAP.Common.Congrats" = "Gratulacje!"; -"IAP.Common.ValidateError" = "Nie można zweryfikować twojego zakupu."; -"IAP.Common.Connecting" = "Łączenie z AppStore…"; -"Premium.OnetapTranslate" = "Przycisk Szybkie tłumaczenie"; -"Premium.IgnoreTranslate.Title" = "Ignorowane języki"; -"Premium.IgnoreTranslate.Header" = "Przycisk „Szybkie tłumaczenie” zostanie WYŁĄCZONY dla języków, które wybierzesz poniżej."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Zarządzaj zakładkami"; -"ManageFilters.Header" = "SKONFIGURUJ DOSTĘPNE FILTRY"; - -"Messages.Translate" = "Tłumacz"; -"Messages.UndoTranslate" = "Cofnij tłumaczenie"; -"Messages.TranslateError" = "Tłumaczenie niedostępne."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Uzyskaj datę rejestracji"; -"NGLab.RegDate.Title" = "Data rejestracji"; -"NGLab.RegDate.Notice" = "To jest przybliżona data"; -"NGLab.RegDate.MenuItem" = "zarejestrowano"; -"NGLab.RegDate.FetchError" = "Nie można uzyskać daty rejestracji."; -"NGLab.BadDeviceToken" = "[iOS 11+] Nie można zweryfikować urządzenia."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Synchronizuj ustawienia i foldery z iCloud"; -"NiceFeatures.BackupSettings" = "Kopia zapasowa ustawień i folderów"; -"NiceFeatures.BackupSettings.Notice" = "Tworzy plik kopii zapasowej. Stuknij plik, aby przywrócić."; -"NiceFeatures.BackupSettings.Done" = "Kopia zapasowa została wysłana do Zapisanych wiadomości"; -"NiceFeatures.BackupSettings.Error" = "Błąd tworzenia kopii zapasowej."; - -"NiceFeatures.RestoreSettings.Confirm" = "Czy na pewno chcesz przywrócić foldery i ustawienia z pliku?\n⚠ Zastąpi to bieżące dane"; -"NiceFeatures.RestoreSettings.Done" = "Foldery i ustawienia zostały pomyślnie przywrócone"; -"NiceFeatures.RestoreSettings.Error" = "Błąd podczas przywracania ustawień. Plik może być uszkodzony"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Nie możesz wysyłać wiadomości w trybie Podglądu."; -"Gmod.Unavailable" = "Tryb Podglądu niedostępny"; -"Gmod.Disable.Notice" = "Twój status ostatniej widoczności zostanie ustawiony na «%1»"; -"Gmod" = "Tryb Podglądu"; -"Gmod.Enable" = "Włączyć tryb Podglądu?"; -"Gmod.Disable" = "Wyłączyć tryb Podglądu?"; -"Gmod.Notice" = "Twój status online zostanie ukryty przed wszystkimi w Ustawieniach prywatności Telegrama.\nAplikacja wyświetli ostrzeżenie, jeśli wejdziesz na prywatny czat.\nTwój status online może zostać ujawniony, jeśli wyślesz lub napiszesz JAKĄŚ wiadomość."; - -"SendWithKb" = "Wyślij za pomocą przycisku «Enter»"; -"NiceFeatures.ShowGmodIcon" = "Pokaż ikonę trybu Podglądu"; -"Gmod.OpenChatQ" = "Otworzyć czat?"; -"Gmod.OpenChatNotice" = "Czat zostanie oznaczony jako „przeczytany”, ale możesz wyświetlić podgląd czatu długim naciśnięciem (force-touch)"; -"Gmod.OpenChatBtn" = "Tak, otwórz czat"; -"Gmod.DisableBtn" = "Wyłącz ostrzeżenia"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Twój numer zostanie ukryty tylko w interfejsie użytkownika. Aby ukryć go przed innymi, użyj ustawień prywatności."; -"NicegramSettings.Other.showProfileId" = "Pokaż ID"; -"NicegramSettings.Other.showRegDate" = "Pokaż datę rejestracji"; -"Premium.OnetapTranslate.LowPower" = "Rozpoznawanie przy niskiej mocy"; -"Premium.OnetapTranslate.LowPower.Notice" = "Aplikacja wykrywa język każdej wiadomości na urządzeniu. Jest to zadanie o wysokim stopniu zaawansowania, które może mieć wpływ na żywotność baterii."; -"Premium.rememberFolderOnExit" = "Zapamiętaj bieżący folder przy wyjściu"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings deleted file mode 100644 index 21e1e29600e..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Bots"; -"ChatFilter.Channels" = "Canais"; -"ChatFilter.Private" = "Usuários"; -"ChatFilter.Groups" = "Grupos"; -"ChatFilter.Unread" = "Não lidas"; -"ChatFilter.Unmuted" = "Não silenciados"; -"ChatFilter.Favourites" = "Favoritos"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "ABAS"; -"NiceFeatures.Tabs.ShowContacts" = "Mostrar Aba dos Contatos"; -"NiceFeatures.ChatScreen.Header" = "TELA DO CHAT"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Stickers Animados"; -"NiceFeatures.useBackCam" = "Usar a câmera traseira (vídeo mensagens)"; - -"NiceFeatures.Folders.Header" = "PASTAS"; -"NiceFeatures.Folders.TgFolders" = "Pastas embaixo"; -"NiceFeatures.Folders.TgFolders.Notice" = "lista de pastas com estilo iOS"; - -"NiceFeatures.RoundVideos.Header" = "Vídeo mensagens"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Iniciar com a câmera traseira"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Salvar na Nuvem"; - -/*Open Pin*/ -"Chat.OpenPin" = "Mostrar Marcador"; -"ChatFilter.Admin" = "Administrador"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Ocultar conta na notificação"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Em vez de «Usuário → Conta», você verá apenas «Usuário». Para várias contas apenas."; -"NiceFeatures.Notifications.Fix" = "Desativar Notificações Indesejadas"; -"NiceFeatures.Notifications.FixNotice" = "Útil se você receber notificações de chats silenciados.\nA resposta das notificações, rótulos da conta e visualizações da mídia não estarão disponíveis."; -"NiceFeatures.Filters.Header" = "FILTROS (ABAS)"; -"NiceFeatures.Filters.Notice" = "Selecione o número de abas personalizadas.\nToque longo na guia para alterar o filtro."; -"NiceFeatures.Filters.ShowBadge" = "Mostrar Badges (Filtros)"; -"NiceFeatures.UseClassicInfoUi" = "Use UI clássico nas informações do chat"; - -/*Common*/ -"Common.ExitNow" = "Sair Agora"; -"Common.Later" = "Depois"; -"Common.RestartRequired" = "Reinicialização necessária!"; -"NiceFeatures.Tabs.ShowNames" = "Mostrar nomes das abas"; -"Chat.ForwardAsCopy" = "Encaminhar como cópia"; - -/*Folder*/ -"Folder.DefaultName" = "Pasta"; -"Folder.New" = "Nova Pasta"; -"Folder.Created" = "Pasta criada"; -"Folder.AddToExisting" = "Adicionar à pasta existente"; -"Folder.Updated" = "Pasta atualizada"; -"Folder.Create" = "Criar pasta..."; -"Folder.Create.Name" = "Nome da Pasta"; -"Folder.Create.Placeholder" = "Pasta..."; -"Folder.LimitExceeded" = "Desculpe, você não pode criar mais do que 3 pastas personalizadas.\nMais pastas estão disponíveis no Premium."; -"NiceFeatures.HideNumber" = "Ocultar telefone nas configurações"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Indisponível no Nicegram devido às diretrizes da AppStore"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Por favor, use «%1» para configurar o navegador padrão"; -"NiceFeatures.Browser.Header" = "URLs"; -"NiceFeatures.Browser.UseBrowser" = "Abrir Link no Navegador"; -"NiceFeatures.Browser.UseBrowserNotice" = "O Nicegram abrirá links no navegador externo em vez de no aplicativo. O navegador selecionado precisa está instalado."; -"ChatFilter.Missed" = "Perdidas"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "CHATS FIXADOS ILIMITADOS"; -"Premium.SyncPins" = "Sincronizar Chats Fixados"; -"Premium.SyncPins.Notice.ON" = "Se você alterar os chats fixados em outro cliente, eles MUDARÃO no Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Se você alterar os chats fixados em outro cliente, eles NÃO MUDARÃO no Nicegram."; -"Premium.Missed.Header" = "Mensagens perdidas"; -"Premium.Missed" = "Notificar mensagens perdidas"; -"Premium.Missed.Notice" = "Quando você abre o aplicativo após um longo tempo (dormindo, estudando, etc.), o Nicegram te notificará sobre mensagens privadas não lidas e menções."; -"Folder.DeleteAsk" = "Apagar Pasta"; -"Folder.NeedPremium" = "Esta pasta só está disponível com o Premium. Você pode obter a versão Premium ou excluir a pasta com deslize."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Por favor, note que o suporte do Nicegram é feito pelo desenvolvedor e comunidade.\n\nPrimeiramente, dê uma olhada no Nicegram FAQ: ele tem dicas e respostas importantes para a maioria das perguntas."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Recursos exclusivos que você não pode recusar!"; -"IAP.Premium.Features" = "Lembre-se da pasta selecionada ao sair"; -"IAP.Premium.Activated" = "Premium ativado!"; -"IAP.Common.Restore" = "Restaurar Compras"; -"IAP.Common.CantPay" = "Desculpe, mas você não pode fazer compras devido a restrições no seu dispositivo ou conta."; -"IAP.Common.ErrorFetch" = "Desculpe, não é possível obter compras na App Store."; -"IAP.Common.Congrats" = "Parabéns!"; -"IAP.Common.ValidateError" = "Desculpe, não é possível validar sua compra."; -"IAP.Common.Connecting" = "Contactando AppStore..."; -"Premium.OnetapTranslate" = "Botão de Tradução Rápida"; -"Premium.IgnoreTranslate.Title" = "Idiomas Ignorados"; -"Premium.IgnoreTranslate.Header" = "O botão De traduzir esta DESATIVADO para os idiomas que você escolher abaixo."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Administrar Abas"; -"ManageFilters.Header" = "CONFIGURAR FILTROS DISPONÍVEIS"; - -"Messages.Translate" = "Traduzir"; -"Messages.UndoTranslate" = "Desfazer Tradução"; -"Messages.TranslateError" = "Desculpe, tradução indisponível."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Obter Data de Registro"; -"NGLab.RegDate.Title" = "Data de Registro"; -"NGLab.RegDate.Notice" = "Esta é uma data aproximada"; -"NGLab.RegDate.MenuItem" = "registrado"; -"NGLab.RegDate.FetchError" = "Desculpe, não foi possível obter a data de registro."; -"NGLab.BadDeviceToken" = "Incapaz de verificar o dispositivo."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Sincronizar configurações e pastas com o iCloud"; -"NiceFeatures.BackupSettings" = "Backup das Configurações e Pastas"; -"NiceFeatures.BackupSettings.Notice" = "Cria um arquivo de backup. Toque no arquivo para restaurar."; -"NiceFeatures.BackupSettings.Done" = "Backup enviado para Mensagens Salvas"; -"NiceFeatures.BackupSettings.Error" = "Erro ao criar backup"; - -"NiceFeatures.RestoreSettings.Confirm" = "Tem certeza de que deseja restaurar pastas e configurações do arquivo?\n⚠️ Ele irá substituir os dados atuais"; -"NiceFeatures.RestoreSettings.Done" = "Pastas e Configurações restauradas com sucesso"; -"NiceFeatures.RestoreSettings.Error" = "Erro ao restaurar as configurações. O arquivo pode estar corrompido"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Você não pode enviar mensagens no Modo de Pré-visualização."; -"Gmod.Unavailable" = "Modo de Pré-visualização Indisponível"; -"Gmod.Disable.Notice" = "A visibilidade da última visualização será definida para «%1»"; -"Gmod" = "Modo Pré-visualização"; -"Gmod.Enable" = "Ativar modo pré-visualização?"; -"Gmod.Disable" = "Desativar modo de pré-visualização?"; -"Gmod.Notice" = "Seu status online será ocultado para todos pelas configurações de privacidade do Telegram.\nO aplicativo irá mostrar um aviso se você estiver entrando em um chat privado.\nSeu status online pode ser revelado se você enviar ou digitar QUALQUER mensagem."; - -"SendWithKb" = "Enviar usando o botão «Enter»"; -"NiceFeatures.ShowGmodIcon" = "Mostrar ícone do modo pré-visualização"; -"Gmod.OpenChatQ" = "Abrir Chat?"; -"Gmod.OpenChatNotice" = "O chat será marcado como \"lido\", mas você pode pré-visualizar o chat com um toque longo.(3D Touch)"; -"Gmod.OpenChatBtn" = "Sim, abra o chat"; -"Gmod.DisableBtn" = "Desativar avisos"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Seu número ficará oculto apenas na interface do usuário. Para ocultá-lo de outras pessoas, use as configurações de privacidade."; -"NicegramSettings.Other.showProfileId" = "Mostrar perfil"; -"NicegramSettings.Other.showRegDate" = "Mostrar data de registro"; -"Premium.OnetapTranslate.LowPower" = "Reconhecimento em modo pouca energia"; -"Premium.OnetapTranslate.LowPower.Notice" = "O aplicativo detecta o idioma de cada mensagem em seu dispositivo. É uma tarefa de alto desempenho que pode afetar a vida útil da bateria."; -"Premium.rememberFolderOnExit" = "Lembrar pasta atual antes de sair"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings deleted file mode 100644 index a3e04426f92..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,114 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Boți"; -"ChatFilter.Channels" = "Canale"; -"ChatFilter.Private" = "Utilizatori"; -"ChatFilter.Groups" = "Grupuri"; -"ChatFilter.Unread" = "Necitit"; -"ChatFilter.Unmuted" = "Unmute"; -"ChatFilter.Favourites" = "Favorite"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "FERESTRE"; -"NiceFeatures.Tabs.ShowContacts" = "Vizualizare contacte"; -"NiceFeatures.ChatScreen.Header" = "AFIȘARE CHAT"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Stickere Animate"; -"NiceFeatures.useBackCam" = "Utilizați camera din spate (videoclipuri rotunde)"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Salvează în Cloud"; - -/*Open Pin*/ -"Chat.OpenPin" = "Afișare Pin"; -"ChatFilter.Admin" = "Administratori"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Ascundeți contul în notificare"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "În loc de «Utilizator → Cont», veți vedea doar «Utilizator». Numai pentru mai multe conturi."; -"NiceFeatures.Notifications.Fix" = "Dezactivare notificări standard"; -"NiceFeatures.Notifications.FixNotice" = "Este util dacă primiți notificări de la chat-urile dezactivate.\nRăspunsul de la notificări, etichetele contului și previzualizările media nu vor fi disponibile."; -"NiceFeatures.Filters.Header" = "FILTRE (FERESTRE)"; -"NiceFeatures.Filters.Notice" = "Selectați numărul de file personalizate.\nApăsați lung pe filă pentru a schimba filtrul."; -"NiceFeatures.Filters.ShowBadge" = "Afișați Insigne (Filtre)"; - -/*Common*/ -"Common.ExitNow" = "Ieșiți Acum"; -"Common.Later" = "Mai târziu"; -"Common.RestartRequired" = "Repornire Necesară!"; -"NiceFeatures.Tabs.ShowNames" = "Arată Fereastra cu Numele"; -"Chat.ForwardAsCopy" = "Redirecționeaza ca o Copie"; - -/*Folder*/ -"Folder.DefaultName" = "Dosar"; -"Folder.New" = "Dosar nou"; -"Folder.Created" = "Dosar creat"; -"Folder.AddToExisting" = "Adaugă la un dosar existent"; -"Folder.Updated" = "Dosar actualizat"; -"Folder.Create" = "Creaţi folderul..."; -"Folder.Create.Name" = "Nume folder"; -"Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Ne pare rău, nu puteți crea mai mult de 3 foldere personalizate.\nMai multe foldere sunt disponibile achiziţionând varianta Premium."; -"NiceFeatures.HideNumber" = "Ascunde telefonul din setări"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Indisponibil în Nicegram datorită Instrucțiunilor din App Store"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Pentru a putea folosi, %1 configurațiva browser-ul prestabilit"; -"NiceFeatures.Browser.Header" = "URL-UL"; -"NiceFeatures.Browser.UseBrowser" = "Deschideți în navigator"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram va deschide legături în browser externe în afara aplicație. Browserul selectat trebuie instalat."; -"ChatFilter.Missed" = "Pierdute"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "CHAT-URI FIXATE NELIMITATE"; -"Premium.SyncPins" = "Sincronizați chat-urile fixate (Pinned)"; -"Premium.SyncPins.Notice.ON" = "Dacă schimbați chat-urile fixate în alt client, acestea se vor schimba în Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Dacă schimbați chat-urile fixate în alt client, acestea NU SE VOR SCHIMBA în Nicegram."; -"Premium.Missed.Header" = "Mesaje ratate"; -"Premium.Missed" = "Notificarea mesajelor pierdute"; -"Premium.Missed.Notice" = "Când deschideți aplicația după o întârziere lungă (dormit, studiat, etc.), Nicegram vă va anunța despre mesaje și mențiuni private necitite."; -"Folder.DeleteAsk" = "Șterge Dosar"; -"Folder.NeedPremium" = "Acest Dosar este destina doar utilizatorilor Premium. Poți obtine Premium sau șterge Dosarul glisând."; -"Common.SupportChatUsername" = "nicegramchat"; -"Common.FAQUrl" = "https://nicegram.app/faq/"; -"Common.FAQ.Button" = "Nicegram Întrebări frecvente"; -"Common.FAQ.Intro" = "Vă rugăm să rețineți că asistența Nicegram este realizată de un singur dezvoltator și de comunitate."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Funcții unice pe care nu le poți refuza!"; -"IAP.Premium.Activated" = "Premium Activat!"; -"IAP.Common.Restore" = "Restabiliţi cumpărăturile"; -"IAP.Common.CantPay" = "Ne pare rău, dar nu puteți face achiziții din cauza restricțiilor de pe dispozitiv sau cont."; -"IAP.Common.ErrorFetch" = "Ne pare rău, nu putem prelua achizițiile din App Store."; -"IAP.Common.Congrats" = "Felicitări!"; -"IAP.Common.ValidateError" = "Ne pare rău, nu putem valida achiziția."; -"Premium.OnetapTranslate" = "Butonul Traducere Rapidă"; -"Premium.IgnoreTranslate.Title" = "Limbi Ignorate"; - -/*Manage Filters*/ -"ManageFilters.Title" = "Administrează Ferestre"; -"ManageFilters.Header" = "CONFIGURĂ FILTRELE DISPONIBILE"; - -"Messages.Translate" = "Traduceți"; -"Messages.UndoTranslate" = "Anulează Traducerea"; -"Messages.TranslateError" = "Ne pare rău, traducere indisponibilă."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Data înregistrării"; -"NGLab.RegDate.Title" = "Data înregistrării"; -"NGLab.RegDate.Notice" = "Aceasta este o dată aproximată"; -"NGLab.RegDate.MenuItem" = "înregistrat"; -"NGLab.RegDate.FetchError" = "Ne pare rău, nu putem obține data înregistrării."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Sincronizați Setările și Dosarele cu iCloud"; -"NiceFeatures.BackupSettings" = "Copie de rezerva(Backup) Setări & Dosare"; -"NiceFeatures.BackupSettings.Notice" = "Creează copie de siguranță fișiere. Atingeți fișierul pentru restaurare."; -"NiceFeatures.BackupSettings.Done" = "Copie de rezervă facută pentru Mesajele Salvate"; -"NiceFeatures.BackupSettings.Error" = "Eroare la crearea copiei de rezervă"; - -"NiceFeatures.RestoreSettings.Confirm" = "Sigur doriți să restaurați folderele și setările din fișier?\n⚠️ Va anula datele actuale"; -"NiceFeatures.RestoreSettings.Done" = "Dosare & Setarile restaurat cu succes"; -"NiceFeatures.RestoreSettings.Error" = "Eroare restabilire setări. Fișierle pot fi corupte"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings deleted file mode 100644 index 2635aa06513..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,148 +0,0 @@ -"AppName" = "Найсграм"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Боты"; -"ChatFilter.Channels" = "Каналы"; -"ChatFilter.Private" = "Пользователи"; -"ChatFilter.Groups" = "Группы"; -"ChatFilter.Unread" = "Непрочитанные"; -"ChatFilter.Unmuted" = "Со звуком"; -"ChatFilter.Favourites" = "Важные"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Найсграм"; -"NiceFeatures.Tabs.Header" = "ВКЛАДКИ"; -"NiceFeatures.Tabs.ShowContacts" = "Вкладка «Контакты»"; -"NiceFeatures.ChatScreen.Header" = "ЧАТ"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Анимированные Стикеры"; -"NiceFeatures.useBackCam" = "Задняя камера (Видеосообщения)"; - -"NiceFeatures.Folders.Header" = "ПАПКИ"; -"NiceFeatures.Folders.TgFolders" = "Папки снизу"; -"NiceFeatures.Folders.TgFolders.Notice" = "Список папок в стиле iOS"; - -"NiceFeatures.RoundVideos.Header" = "ВИДЕОСООБЩЕНИЯ"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Запись сразу с задней камеры"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Сохранить в Избранное"; - -/*Open Pin*/ -"Chat.OpenPin" = "Показать Пин"; -"ChatFilter.Admin" = "Админ"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Скрыть аккаунт в уведомлении"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "Вместо «Пользователь → Аккаунт» вы будете видеть только «Пользователь». Только для нескольких аккаунтов."; -"NiceFeatures.Notifications.Fix" = "Отключить нежелательные уведомления"; -"NiceFeatures.Notifications.FixNotice" = "Нужно, если вы получаете уведомления, даже если они отключены в чате. Ответ из уведомления, метки аккаунтов и предпросмотр медиа будут недоступны."; -"NiceFeatures.Filters.Header" = "ФИЛЬТРЫ (ВКЛАДКИ)"; -"NiceFeatures.Filters.Notice" = "Выберите количество пользовательских вкладок.\nДолгое нажатие по вкладке позволит сменить фильтр."; -"NiceFeatures.Filters.ShowBadge" = "Показывать бейджи (Фильтры)"; -"NiceFeatures.UseClassicInfoUi" = "Классический интерфейс «о чате»"; - -/*Common*/ -"Common.ExitNow" = "Выйти Сейчас"; -"Common.Later" = "Позже"; -"Common.RestartRequired" = "Необходим перезапуск!"; -"NiceFeatures.Tabs.ShowNames" = "Показывать Имена Вкладок"; -"Chat.ForwardAsCopy" = "Переслать без Автора"; - -/*Folder*/ -"Folder.DefaultName" = "Папка"; -"Folder.New" = "Новая Папка"; -"Folder.Created" = "Папка создана"; -"Folder.AddToExisting" = "Добавить в Папку"; -"Folder.Updated" = "Папка обновлена"; -"Folder.Create" = "Создать Папку..."; -"Folder.Create.Name" = "Название Папки"; -"Folder.Create.Placeholder" = "Папка..."; -"Folder.LimitExceeded" = "К сожалению, создать можно не больше 3 папок. Совсем скоро можно будет создать больше."; -"NiceFeatures.HideNumber" = "Скрыть номер в настройках"; - -/*NGWeb*/ -"NGWeb.Blocked" = "Недоступно в Nicegram по правилам AppStore"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Пожалуйста, используйте «%1» для настройки браузера по умолчанию"; -"NiceFeatures.Browser.Header" = "ССЫЛКИ"; -"NiceFeatures.Browser.UseBrowser" = "Открывать во внешнем браузере"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram будет открывать ссылки во внешнем браузере вместо встроенного. Выбранный браузер должен быть установлен."; -"ChatFilter.Missed" = "Новые"; - -/*Premium*/ -"Premium.Title" = "Премиум"; -"Premium.UnlimitedPins.Header" = "НЕОГРАНИЧЕННЫЕ ЗАКРЕПЛЁННЫЕ ЧАТЫ"; -"Premium.SyncPins" = "Синхронизировать Чаты"; -"Premium.SyncPins.Notice.ON" = "Если вы измените закрепленные чаты в другом клиенте, они ИЗМЕНЯТСЯ в Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Если вы измените закрепленные чаты в другом клиенте, они НЕ ИЗМЕНЯТСЯ в Nicegram."; -"Premium.Missed.Header" = "Новые сообщения"; -"Premium.Missed" = "Уведомлять о пропущенных"; -"Premium.Missed.Notice" = "Когда вы открываете приложение после долгого отсутствия (сон, работа и т.д.), Nicegram откроет вкладку с непрочитанными сообщениями и упоминаниями."; -"Folder.DeleteAsk" = "Удалить папку"; -"Folder.NeedPremium" = "Эта Папка доступна только с Премиумом. Вы можете получить Премиум или удалить папку свайпом."; -"Common.SupportChatUsername" = "nicegram_ru"; -"Common.FAQUrl" = "https://nicegram.app/ru/faq/"; -"Common.FAQ.Button" = "Вопросы о Nicegram"; -"Common.FAQ.Intro" = "Пожалуйста, обратите внимание, что поддержка Nicegram осуществляется одним единственным разработчиком и сообществом.\n\nВ первую очередь, ознакомьтесь с часто задаваемыми вопросами о Nicegram: там Вы найдёте ответы на большинство вопросов и важные советы по устранению неполадок."; -"IAP.Premium.Title" = "Премиум"; -"IAP.Premium.Subtitle" = "Уникальные функции, от которых трудно отказаться!"; -"IAP.Premium.Features" = "Быстрая кнопка перевода\n\nЗапоминание папки при выходе"; -"IAP.Premium.Activated" = "Премиум Активирован!"; -"IAP.Common.Restore" = "Восстановить Покупки"; -"IAP.Common.CantPay" = "К сожалению, Вы не можете производить покупки ввиду ограничений вашего устройства или аккаунта."; -"IAP.Common.ErrorFetch" = "Не удалось связаться с AppStore"; -"IAP.Common.Congrats" = "Поздравляем!"; -"IAP.Common.ValidateError" = "К сожалению, подтвердить покупку не удалось."; -"IAP.Common.Connecting" = "Подключение к AppStore..."; -"Premium.OnetapTranslate" = "Кнопка быстрого перевода"; -"Premium.IgnoreTranslate.Title" = "Игнорируемые языки"; -"Premium.IgnoreTranslate.Header" = "Кнопка быстрого перевода будет ОТКЛЮЧЕНА для выбранных ниже языков."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Управление вкладками"; -"ManageFilters.Header" = "РЕДАКТИРОВАНИЕ ДОСТУПНЫХ ФИЛЬТРОВ"; - -"Messages.Translate" = "Перевод"; -"Messages.UndoTranslate" = "Отменить Перевод"; -"Messages.TranslateError" = "К сожалению, перевод недоступен."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Узнать дату регистрации"; -"NGLab.RegDate.Title" = "Дата регистрации"; -"NGLab.RegDate.Notice" = "Это примерная дата"; -"NGLab.RegDate.MenuItem" = "регистрация"; -"NGLab.RegDate.FetchError" = "К сожалению, дата регистрации недоступна."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Синхронизация Настроек и Папок в iCloud"; -"NiceFeatures.BackupSettings" = "Резервное копирование настроек и папок"; -"NiceFeatures.BackupSettings.Notice" = "Создает бэкап файл. Тапни на файл для восстановления."; -"NiceFeatures.BackupSettings.Done" = "Резервное копирование выполнено в сохраненные сообщения"; -"NiceFeatures.BackupSettings.Error" = "Ошибка при создании бэкапа"; - -"NiceFeatures.RestoreSettings.Confirm" = "Восстановить Папки и Настройки из файла?\n⚠️ Текущие данные будут перезаписаны"; -"NiceFeatures.RestoreSettings.Done" = "Папки и настройки успешно восстановлены"; -"NiceFeatures.RestoreSettings.Error" = "Ошибка при восстановлении настроек. Возможно, файл повреждён"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Вы не можете отправлять сообщения в режиме предпросмотра."; -"Gmod.Unavailable" = "Режим предпросмотра недоступен"; -"Gmod.Disable.Notice" = "Настройка видимости онлайн статуса будет установлена в «%1»"; -"Gmod" = "Режим предпросмотра"; -"Gmod.Enable" = "Включить режим предпросмотра?"; -"Gmod.Disable" = "Отключить режим предпросмотра?"; -"Gmod.Notice" = "Ваш онлайн статус будет скрыт от всех в настройках конфиденциальности Telegram.\nПриложение предупредит, если вы захотите открыть личный чат.\nВаш онлайн статус может быть виден, если вы отправите или напечатаете ЛЮБОЕ сообщение."; - -"SendWithKb" = "Отправка кнопкой «Enter»"; -"NiceFeatures.ShowGmodIcon" = "Иконка режима предпросмотра"; -"Gmod.OpenChatQ" = "Открыть чат?"; -"Gmod.OpenChatNotice" = "Чат будет помечен как \"прочитанный\", но вы можете просмотреть чат, не помечая его, долгим нажатием"; -"Gmod.OpenChatBtn" = "Да, открыть чат"; -"Gmod.DisableBtn" = "Отключить предупреждения"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Ваш номер будет скрыт только в интерфейсе. Чтобы скрыть его от других, пожалуйста, используйте настройки конфиденциальности."; -"NicegramSettings.Other.showProfileId" = "Показывать ID профилей"; -"NicegramSettings.Other.showRegDate" = "Показывать дату регистрации"; -"Premium.OnetapTranslate.LowPower" = "Распознавание в режиме энергосбережения"; -"Premium.OnetapTranslate.LowPower.Notice" = "Приложение определяет язык каждого сообщения с помощью вашего устройства. Это ресурсоёмкая задача, которая может повлиять на время работы аккумулятора."; -"Premium.rememberFolderOnExit" = "Запомнить текущую папку при выходе"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/sk.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/sk.lproj/NiceLocalizable.strings deleted file mode 100644 index 25e8a999ae9..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/sk.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,58 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Boti"; -"ChatFilter.Channels" = "Kanály"; -"ChatFilter.Private" = "Užívatelia"; -"ChatFilter.Groups" = "Skupiny"; -"ChatFilter.Unread" = "Neprečítané"; -"ChatFilter.Unmuted" = "Nahlas"; -"ChatFilter.Favourites" = "Obľúbené"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "ZÁLOŽKY"; -"NiceFeatures.Tabs.ShowContacts" = "Zobraziť kontakty"; -"NiceFeatures.ChatScreen.Header" = "CHATOVÁ OBRAZOVKA"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animované nálepky"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Uložiť na Cloud"; - -/*Open Pin*/ -"Chat.OpenPin" = "Zobraziť pripnuté"; -"ChatFilter.Admin" = "Admin"; -"NiceFeatures.Notifications.Fix" = "Vypnúť nechcené notifikácie"; -"NiceFeatures.Notifications.FixNotice" = "Nápomocné, pri dostávaní notifikácií zo stlmených chatov.\nOdpovede z notifikácií, názvy účtov a náhľady médií nebudú dostupné."; -"NiceFeatures.Filters.Header" = "FILTRE (ZÁLOŽKY)"; -"NiceFeatures.Filters.Notice" = "Zvoľ počet vlastných záložiek.\nPodrž záložku pre zmenenie jej filtra."; - -/*Common*/ -"Common.ExitNow" = "Ukončiť"; -"Common.Later" = "Neskôr"; -"Common.RestartRequired" = "Vyžadovaný reštart!"; -"NiceFeatures.Tabs.ShowNames" = "Zobraziť názvy záložiek"; -"Chat.ForwardAsCopy" = "Preposlať ako kópiu"; - -/*Folder*/ -"Folder.DefaultName" = "Priečinok"; -"Folder.New" = "Nový priečinok"; -"Folder.Created" = "Priečinok bol vytvorený"; -"Folder.AddToExisting" = "Pridať do existujúceho priečinku"; -"Folder.Updated" = "Priečinok aktualizovaný"; -"Folder.Create" = "Vytvoriť priečinok..."; -"Folder.Create.Name" = "Názov priečinku"; -"Folder.Create.Placeholder" = "Priečinok..."; -"NiceFeatures.Browser.UseBrowser" = "Otvoriť linku v prehliadači"; -"Folder.DeleteAsk" = "Odstrániť priečinok"; -"IAP.Premium.Title" = "Prémium"; -"IAP.Premium.Activated" = "Prémium Aktivované!"; -"IAP.Common.Restore" = "Obnoviť nákup"; -"IAP.Common.Congrats" = "Gratulujeme!"; - -"Messages.Translate" = "Preložiť"; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Dátum registrácie"; -"NGLab.RegDate.Title" = "Dátum registrácie"; -"NGLab.RegDate.MenuItem" = "zaregistrovaný"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings deleted file mode 100644 index 86f6e7a3060..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Botlar"; -"ChatFilter.Channels" = "Kanallar"; -"ChatFilter.Private" = "Kullanıcılar"; -"ChatFilter.Groups" = "Gruplar"; -"ChatFilter.Unread" = "Okunmamış"; -"ChatFilter.Unmuted" = "Sessize Alınmış"; -"ChatFilter.Favourites" = "Favoriler"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "SEKMELER"; -"NiceFeatures.Tabs.ShowContacts" = "Kişiler Sekmesini Göster"; -"NiceFeatures.ChatScreen.Header" = "SOHBET EKRANI"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Animasyonlu Çıkartmalar"; -"NiceFeatures.useBackCam" = "Arka kamerayı kullan (Yuvarlak Videolar)"; - -"NiceFeatures.Folders.Header" = "KLASÖRLER"; -"NiceFeatures.Folders.TgFolders" = "Alttaki Klasörler"; -"NiceFeatures.Folders.TgFolders.Notice" = "iOS tarzı klasör listesi"; - -"NiceFeatures.RoundVideos.Header" = "YUVARLAK VİDEOLAR"; -"NiceFeatures.RoundVideos.UseRearCamera" = "Arka kamerayla başlat"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Buluta Kaydet"; - -/*Open Pin*/ -"Chat.OpenPin" = "Sabiti Göster"; -"ChatFilter.Admin" = "Yönetici"; -"NiceFeatures.Notifications.HideNotifyAccount" = "Hesabı bildirimde gizle"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "«Kullanıcı → Hesap» yerine sadece «Kullanıcı» göreceksiniz. Yalnızca çoklu hesaplar için."; -"NiceFeatures.Notifications.Fix" = "İstenmeyen Bildirimleri Devre Dışı Bırak"; -"NiceFeatures.Notifications.FixNotice" = "Sessiz sohbetlerden bildirimler alıyorsanız kullanışlıdır.\nBildirimlerden yanıtlama, hesap etiketleri ve medya önizlemeleri kullanılamaz olacak."; -"NiceFeatures.Filters.Header" = "FİLTRELER (SEKMELER)"; -"NiceFeatures.Filters.Notice" = "Özel sekmelerin sayısını seçin.\nFiltreyi değiştirmek için sekmeye uzun basın."; -"NiceFeatures.Filters.ShowBadge" = "Rozetleri Göster (Filtreler)"; -"NiceFeatures.UseClassicInfoUi" = "Klasik Sohbet Bilgisi Arayüzünü Kullan"; - -/*Common*/ -"Common.ExitNow" = "Şimdi Çık"; -"Common.Later" = "Sonra"; -"Common.RestartRequired" = "Yeniden Başlatma Gerekli!"; -"NiceFeatures.Tabs.ShowNames" = "Sekme İsimlerini Göster"; -"Chat.ForwardAsCopy" = "Kopya Olarak İlet"; - -/*Folder*/ -"Folder.DefaultName" = "Klasör"; -"Folder.New" = "Yeni Klasör"; -"Folder.Created" = "Klasör oluşturuldu"; -"Folder.AddToExisting" = "Var Olan Klasöre Ekle"; -"Folder.Updated" = "Klasör güncellendi"; -"Folder.Create" = "Klasör Oluştur..."; -"Folder.Create.Name" = "Klasör İsmi"; -"Folder.Create.Placeholder" = "Klasör..."; -"Folder.LimitExceeded" = "Üzgünüz, 3’ten fazla özel klasör oluşturamazsınız.\nDaha fazla klasör Premium’da kullanılabilir."; -"NiceFeatures.HideNumber" = "Ayarlarda numarayı gizle"; - -/*NGWeb*/ -"NGWeb.Blocked" = "AppStore Kuralları yüzünden Nicegram’da kullanılamıyor"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "Lütfen, varsayılan tarayıcıyı ayarlamak için «%1» kısmını kullanın"; -"NiceFeatures.Browser.Header" = "Bağlantılar"; -"NiceFeatures.Browser.UseBrowser" = "Bağlantıları tarayıcıda aç"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram, bağlantıları uygulama içi yerine harici tarayıcıda açacak. Seçilen tarayıcı yüklü olmalı."; -"ChatFilter.Missed" = "Kaçırılan"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "SINIRSIZ SABİTLENMİŞ SOHBET"; -"Premium.SyncPins" = "Senkronize Sabitlenmiş Sohbetler"; -"Premium.SyncPins.Notice.ON" = "Eğer diğer sistemlerinizde sabit sohbetlerinizi değiştirirseniz, Nicegram’da da DEĞİŞECEKTİR."; -"Premium.SyncPins.Notice.OFF" = "Eğer diğer sistemlerinizde sabit sohbetlerinizi değiştirirseniz, Nicegram’da DEĞİŞMEYECEKTİR."; -"Premium.Missed.Header" = "Kaçırılan mesajlar"; -"Premium.Missed" = "Kaçırılan mesajlarda bildirim"; -"Premium.Missed.Notice" = "Uzun bir süreden sonra (uyku, çalışma vb.) uygulamayı açtığınızda, Nicegram size okunmamış özel mesajlar ve bahsedilmeler hakkında bildirim gönderecek."; -"Folder.DeleteAsk" = "Klasörü Sil"; -"Folder.NeedPremium" = "Bu klasör sadece Premium ile kullanılabilir. Premium’u alabilir ya da kaydırarak klasörü silebilirsiniz."; -"Common.SupportChatUsername" = "nicegram_tr"; -"Common.FAQUrl" = "https://nicegram.app/tr/faq/"; -"Common.FAQ.Button" = "Nicegram SSS"; -"Common.FAQ.Intro" = "Lütfen Nicegram Destek’in sadece geliştirici ve topluluktan ibaret olduğunu unutmayın.\n\nİlk olarak, Nicegram SSS’e göz atın: önemli çözüm ipuçları ve birçok soruya cevaplar var."; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "Reddedemeyeceğiniz benzersiz özellikler!"; -"IAP.Premium.Features" = "Hızlı Mesaj Çevirmeni\n\nÇıkışta seçili klasörü hatırla"; -"IAP.Premium.Activated" = "Premium Aktive Edildi!"; -"IAP.Common.Restore" = "Satın Alımları Geri Yükle"; -"IAP.Common.CantPay" = "Üzgünüz, ama cihaz veya hesap kısıtlamalarınız yüzünden satın alım yapamazsınız."; -"IAP.Common.ErrorFetch" = "Üzgünüz, App Store satın alımları çekilemiyor."; -"IAP.Common.Congrats" = "Tebrikler!"; -"IAP.Common.ValidateError" = "Üzgünüz, satın alımınız doğrulanamıyor."; -"IAP.Common.Connecting" = "AppStore'a bağlanılıyor..."; -"Premium.OnetapTranslate" = "Hızlı Çeviri butonu"; -"Premium.IgnoreTranslate.Title" = "İstenmeyen Diller"; -"Premium.IgnoreTranslate.Header" = "Hızlı Çeviri butonu, aşağıda seçtiğiniz diller için DEVRE DIŞI bırakılacaktır."; - -/*Manage Filters*/ -"ManageFilters.Title" = "Sekmeleri Yönet"; -"ManageFilters.Header" = "KULLANILAN FİLTRELERİ ÖZELLEŞTİR"; - -"Messages.Translate" = "Çevir"; -"Messages.UndoTranslate" = "Çeviriyi Geri Al"; -"Messages.TranslateError" = "Üzgünüz, çeviri kullanılamıyor."; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "Kayıt Tarihini Göster"; -"NGLab.RegDate.Title" = "Kayıt Tarihi"; -"NGLab.RegDate.Notice" = "Bu yaklaşık bir tarihtir"; -"NGLab.RegDate.MenuItem" = "kayıtlı"; -"NGLab.RegDate.FetchError" = "Üzgünüz, kayıt tarihi gösterilemiyor."; -"NGLab.BadDeviceToken" = "[iOS 11+] Cihaz doğrulanamıyor."; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "Ayarları & Klasörleri iCloud ile Senkronize Et"; -"NiceFeatures.BackupSettings" = "Ayarları & Klasörleri Yedekle"; -"NiceFeatures.BackupSettings.Notice" = "Yedekleme dosyası oluşturur. Geri yüklemek için dosyaya dokunun."; -"NiceFeatures.BackupSettings.Done" = "Yedekleme, Kayıtlı Mesajlar’a gönderildi"; -"NiceFeatures.BackupSettings.Error" = "Yedekleme oluşturulurken hata"; - -"NiceFeatures.RestoreSettings.Confirm" = "Klasör & Ayarları dosyadan geri yüklemek istediğinize emin misiniz?\n⚠️ Şu ankinin üzerine yazılacak"; -"NiceFeatures.RestoreSettings.Done" = "Klasör & Ayarlar başarılı bir şekilde geri yüklendi"; -"NiceFeatures.RestoreSettings.Error" = "Ayarlar geri yüklenirken hata. Dosya bozulmuş olabilir"; - -/*Preview Mode*/ -"Gmod.Restricted" = "Önizleme Modu’nda mesaj gönderemezsiniz."; -"Gmod.Unavailable" = "Önizleme Modu Kullanılamaz"; -"Gmod.Disable.Notice" = "Son görülme görünürlüğünüz, «%1» olarak ayarlanacak"; -"Gmod" = "Önizleme Modu"; -"Gmod.Enable" = "Önizleme Modu etkinleştirilsin mi?"; -"Gmod.Disable" = "Önizleme Modu devre dışı bırakılsın mı?"; -"Gmod.Notice" = "Çevrimiçi durumunuz, Telegram gizlilik ayarlarına göre herkesten gizlenecek.\nUygulama, siz özel bir sohbete girerken bir uyarı gösterecek.\nHERHANGİ BİR mesaj yazar veya gönderirseniz, çevrimiçi durumunuz görünebilir."; - -"SendWithKb" = "«Enter» butonu ile gönder"; -"NiceFeatures.ShowGmodIcon" = "Önizleme Modu simgesini göster"; -"Gmod.OpenChatQ" = "Sohbet açılsın mı?"; -"Gmod.OpenChatNotice" = "Sohbet “okundu” olarak işaretlenecek, ama uzun dokunma ile sohbeti önizleyebilirsiniz (force-touch)"; -"Gmod.OpenChatBtn" = "Evet, sohbeti aç"; -"Gmod.DisableBtn" = "Uyarıları devre dışı bırak"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Numaranız sadece arayüzde gizlenecek. Diğerlerinden gizlemek için, lütfen Gizlilik ayarlarını kullanın."; -"NicegramSettings.Other.showProfileId" = "Profil ID'sini Göster"; -"NicegramSettings.Other.showRegDate" = "Kayıt Tarihini Göster"; -"Premium.OnetapTranslate.LowPower" = "Düşük Güçte Tanıma"; -"Premium.OnetapTranslate.LowPower.Notice" = "Uygulama, cihazınızdaki her mesajın dilini algılar. Bu, pil ömrünüzü etkileyebilecek yüksek öncelikli bir görevdir."; -"Premium.rememberFolderOnExit" = "Çıkışta şu anki klasörü hatırla"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings deleted file mode 100644 index 747db00b740..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,81 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "Боти"; -"ChatFilter.Channels" = "Канали"; -"ChatFilter.Private" = "Користувачі"; -"ChatFilter.Groups" = "Групи"; -"ChatFilter.Unread" = "Непрочитане"; -"ChatFilter.Unmuted" = "Нотифiкувати"; -"ChatFilter.Favourites" = "Обране"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "ВКЛАДКИ"; -"NiceFeatures.Tabs.ShowContacts" = "Показувати контакти"; -"NiceFeatures.ChatScreen.Header" = "ВIКНО ЧАТУ"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "Анiмованi стiкери"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "Зберегти До Хмари"; - -/*Open Pin*/ -"Chat.OpenPin" = "Показати Пiн"; -"ChatFilter.Admin" = ""; -"NiceFeatures.Notifications.Fix" = "Вимкнути небажані оповістки"; -"NiceFeatures.Notifications.FixNotice" = "Допомагає, коли ви отримуєте оповістки від заглушених чатів. Відповіді з оповісток, назви акаунтів і попередній перегляд медіа буде недоступний"; -"NiceFeatures.Filters.Header" = ""; -"NiceFeatures.Filters.Notice" = ""; - -/*Common*/ -"Common.ExitNow" = ""; -"Common.Later" = ""; -"Common.RestartRequired" = ""; -"NiceFeatures.Tabs.ShowNames" = ""; -"Chat.ForwardAsCopy" = ""; - -/*Folder*/ -"Folder.DefaultName" = ""; -"Folder.New" = ""; -"Folder.Created" = ""; -"Folder.AddToExisting" = ""; -"Folder.Updated" = ""; -"Folder.Create" = ""; -"Folder.Create.Name" = ""; -"Folder.Create.Placeholder" = ""; -"Folder.LimitExceeded" = ""; -"NiceFeatures.HideNumber" = ""; - -/*NGWeb*/ -"NGWeb.Blocked" = ""; - -/*Browser*/ -"NiceFeatures.Browser.Header" = ""; -"NiceFeatures.Browser.UseBrowser" = ""; -"NiceFeatures.Browser.UseBrowserNotice" = ""; -"ChatFilter.Missed" = ""; - -/*Premium*/ -"Premium.Title" = ""; -"Premium.UnlimitedPins.Header" = ""; -"Premium.SyncPins" = ""; -"Premium.SyncPins.Notice.ON" = ""; -"Premium.SyncPins.Notice.OFF" = ""; -"Premium.Missed.Header" = ""; -"Premium.Missed" = ""; -"Premium.Missed.Notice" = ""; -"Folder.DeleteAsk" = ""; -"Folder.NeedPremium" = ""; -"Common.SupportChatUsername" = "nicegram_ru"; -"Common.FAQUrl" = "https://nicegram.app/ru/faq/"; -"Common.FAQ.Button" = ""; -"Common.FAQ.Intro" = ""; -"IAP.Premium.Title" = ""; -"IAP.Premium.Subtitle" = ""; -"IAP.Premium.Features" = ""; -"IAP.Premium.Activated" = ""; -"IAP.Common.Restore" = ""; -"IAP.Common.CantPay" = ""; -"IAP.Common.ErrorFetch" = ""; -"IAP.Common.Congrats" = ""; -"IAP.Common.ValidateError" = ""; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings deleted file mode 100644 index 2b0a4f2f92d..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "机器人"; -"ChatFilter.Channels" = "频道"; -"ChatFilter.Private" = "用户"; -"ChatFilter.Groups" = "群组"; -"ChatFilter.Unread" = "未读信息"; -"ChatFilter.Unmuted" = "已启用通知"; -"ChatFilter.Favourites" = "收藏夹"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "标签"; -"NiceFeatures.Tabs.ShowContacts" = "显示<联系人>标签"; -"NiceFeatures.ChatScreen.Header" = "对话界面"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "动态贴纸"; -"NiceFeatures.useBackCam" = "使用后置摄像头(圆形视频)"; - -"NiceFeatures.Folders.Header" = "分组"; -"NiceFeatures.Folders.TgFolders" = "分组显示在底部"; -"NiceFeatures.Folders.TgFolders.Notice" = "iOS 风格分组列表"; - -"NiceFeatures.RoundVideos.Header" = "圆形视频"; -"NiceFeatures.RoundVideos.UseRearCamera" = "使用后置相机"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "保存到收藏夹"; - -/*Open Pin*/ -"Chat.OpenPin" = "显示置顶"; -"ChatFilter.Admin" = "管理员"; -"NiceFeatures.Notifications.HideNotifyAccount" = "在通知中隐藏帐户"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "当您在设置添加了多个账户时,通知中就不会显示你的账户信息。"; -"NiceFeatures.Notifications.Fix" = "禁用多余通知"; -"NiceFeatures.Notifications.FixNotice" = "如果在已关闭通知群组/频道仍然会收到通知,那开启此功能可以禁用此通知.\n开启此功能,在通知界面回复消息,帐户标签和媒体预览将会被禁用。"; -"NiceFeatures.Filters.Header" = "标签过滤"; -"NiceFeatures.Filters.Notice" = "选择自定义标签的数量。\n长按标签更改标签选项。"; -"NiceFeatures.Filters.ShowBadge" = "标签上显示角标"; -"NiceFeatures.UseClassicInfoUi" = "使用经典对话信息界面"; - -/*Common*/ -"Common.ExitNow" = "立即退出"; -"Common.Later" = "稍后"; -"Common.RestartRequired" = "需要重启!"; -"NiceFeatures.Tabs.ShowNames" = "显示标签名"; -"Chat.ForwardAsCopy" = "无引用转发"; - -/*Folder*/ -"Folder.DefaultName" = "分组"; -"Folder.New" = "新建分组"; -"Folder.Created" = "分组已创建"; -"Folder.AddToExisting" = "添加到已有分组"; -"Folder.Updated" = "分组已更新"; -"Folder.Create" = "创建分组..."; -"Folder.Create.Name" = "分组名"; -"Folder.Create.Placeholder" = "分组..."; -"Folder.LimitExceeded" = "抱歉,您最多只能创建3个自定义分组。\n升级至 Premium 可创建更多分组。"; -"NiceFeatures.HideNumber" = "设置中隐藏手机号码"; - -/*NGWeb*/ -"NGWeb.Blocked" = "依据 App Store 审核指南,在 Nicegram 中不可用"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "请使用 «%1» 配置默认浏览器"; -"NiceFeatures.Browser.Header" = "链接"; -"NiceFeatures.Browser.UseBrowser" = "在浏览器中打开链接"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram 将在外部浏览器打开链接而不是在App内打开,必须安装所选择的浏览器。"; -"ChatFilter.Missed" = "未读提醒"; - -/*Premium*/ -"Premium.Title" = "Premium"; -"Premium.UnlimitedPins.Header" = "无限置顶对话"; -"Premium.SyncPins" = "同步置顶对话"; -"Premium.SyncPins.Notice.ON" = "如果您更改其他客户端中的置顶对话,则它们在 Nicegram 中也会同步更改。"; -"Premium.SyncPins.Notice.OFF" = "如果您更改其他客户端中的置顶对话,则它们在 Nicegram 中也不会同步更改。"; -"Premium.Missed.Header" = "未读提醒消息"; -"Premium.Missed" = "未读提醒消息通知"; -"Premium.Missed.Notice" = "长时间未使用 Nicegram (比如睡眠/学习等),下次打开Nicegram会通知您未读的私人和提及消息。"; -"Folder.DeleteAsk" = "删除分组"; -"Folder.NeedPremium" = "此文件夹仅限 Premium 可用。您可以解锁 Premium 来使用,或删除此文件夹。"; -"Common.SupportChatUsername" = "nicegram_cn"; -"Common.FAQUrl" = "https://nicegram.app/cn/faq/"; -"Common.FAQ.Button" = "Nicegram 常见问题"; -"Common.FAQ.Intro" = "请注意:Nicegram 的支持服务由开发者和社区提供。\n\n如用疑问,请前往“Nicegram 常见问题”,那里有常见问题和故障排除指南。"; -"IAP.Premium.Title" = "Premium"; -"IAP.Premium.Subtitle" = "您无法拒绝的独特功能!"; -"IAP.Premium.Features" = "快速消息翻译器\n\n退出时记住选定的文件夹"; -"IAP.Premium.Activated" = "Premium 已激活!"; -"IAP.Common.Restore" = "恢复购买"; -"IAP.Common.CantPay" = "抱歉,由于您的设备或帐户限制,您无法购买。"; -"IAP.Common.ErrorFetch" = "抱歉,无法从 App Store 获取购买信息。"; -"IAP.Common.Congrats" = "恭喜!"; -"IAP.Common.ValidateError" = "抱歉,无法验证您的购买。"; -"IAP.Common.Connecting" = "正在连接到 AppStore..."; -"Premium.OnetapTranslate" = "快速翻译按钮"; -"Premium.IgnoreTranslate.Title" = "忽略的语言"; -"Premium.IgnoreTranslate.Header" = "对于您在下面选择的语言,快速翻译按钮将被禁用。"; - -/*Manage Filters*/ -"ManageFilters.Title" = "管理标签"; -"ManageFilters.Header" = "配置可用的过滤器"; - -"Messages.Translate" = "翻译"; -"Messages.UndoTranslate" = "撤消翻译"; -"Messages.TranslateError" = "抱歉,翻译不可用。"; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "获取注册日期"; -"NGLab.RegDate.Title" = "注册日期"; -"NGLab.RegDate.Notice" = "这是一个大致日期"; -"NGLab.RegDate.MenuItem" = "已注册"; -"NGLab.RegDate.FetchError" = "对不起,无法获得注册日期。"; -"NGLab.BadDeviceToken" = "抱歉,无法验证设备。"; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "使用 iCloud 同步设置和分组"; -"NiceFeatures.BackupSettings" = "备份设置和分组"; -"NiceFeatures.BackupSettings.Notice" = "创建备份文件。点击备份文件恢复。"; -"NiceFeatures.BackupSettings.Done" = "备份已完成,存放在\"收藏夹\"里。"; -"NiceFeatures.BackupSettings.Error" = "创建备份出错"; - -"NiceFeatures.RestoreSettings.Confirm" = "您确定要从备份文件中恢复分组和设置吗?\n⚠️ 这将会覆盖当前的数据"; -"NiceFeatures.RestoreSettings.Done" = "分组和设置已恢复成功"; -"NiceFeatures.RestoreSettings.Error" = "恢复设置出错,备份文件可能已损坏。"; - -/*Preview Mode*/ -"Gmod.Restricted" = "在幽灵模式下,您不能发送消息。"; -"Gmod.Unavailable" = "幽灵模式不可用"; -"Gmod.Disable.Notice" = "您的近期上线状态可见性将会变为«%1»。"; -"Gmod" = "幽灵模式"; -"Gmod.Enable" = "启用幽灵模式?"; -"Gmod.Disable" = "关闭幽灵模式?"; -"Gmod.Notice" = "通过 Telegram 隐私设置,您的在线状态会对所有人隐藏。\n如果您进入私有对话,将会显示警告提示。\n如果您在对话中发送或编辑任何消息,您的在线状态可能会显示出来。"; - -"SendWithKb" = "按<回车>发送"; -"NiceFeatures.ShowGmodIcon" = "显示幽灵模式图标"; -"Gmod.OpenChatQ" = "打开对话?"; -"Gmod.OpenChatNotice" = "对话将被标记为“已读”,不过您可以长按(Force Touch)预览对话"; -"Gmod.OpenChatBtn" = "是的,打开此对话"; -"Gmod.DisableBtn" = "禁用告警提示"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "您的电話号码只会在设置介面中隐藏,要对其他使用者隐藏,请使用隐私设置。"; -"NicegramSettings.Other.showProfileId" = "显示 ID"; -"NicegramSettings.Other.showRegDate" = "显示注册日期"; -"Premium.OnetapTranslate.LowPower" = "低电量时禁用"; -"Premium.OnetapTranslate.LowPower.Notice" = "应用会检测您设备上每条消息的语言。这是一项耗能的任务,会影响您的电池寿命。"; -"Premium.rememberFolderOnExit" = "退出时记住当前分组"; diff --git a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings deleted file mode 100644 index c20b89c66ef..00000000000 --- a/Telegram/Telegram-iOS/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings +++ /dev/null @@ -1,149 +0,0 @@ -"AppName" = "Nicegram"; - -/*ChatFilter*/ -"ChatFilter.Bots" = "機器人"; -"ChatFilter.Channels" = "頻道"; -"ChatFilter.Private" = "用戶"; -"ChatFilter.Groups" = "群組"; -"ChatFilter.Unread" = "未讀訊息"; -"ChatFilter.Unmuted" = "開啟通知"; -"ChatFilter.Favourites" = "常用"; - -/*Nice Features*/ -"NiceFeatures.Title" = "Nicegram"; -"NiceFeatures.Tabs.Header" = "標籤"; -"NiceFeatures.Tabs.ShowContacts" = "顯示聯絡人標籤"; -"NiceFeatures.ChatScreen.Header" = "對話介面"; -"NiceFeatures.ChatScreen.AnimatedStickers" = "動態貼圖"; -"NiceFeatures.useBackCam" = "使用後置鏡頭 (圓形影片)"; - -"NiceFeatures.Folders.Header" = "資料夾"; -"NiceFeatures.Folders.TgFolders" = "底部資料夾"; -"NiceFeatures.Folders.TgFolders.Notice" = "iOS 風格資料夾列表"; - -"NiceFeatures.RoundVideos.Header" = "圓形影片"; -"NiceFeatures.RoundVideos.UseRearCamera" = "開啟後置鏡頭"; - -/*Save to Cloud*/ -"Chat.SaveToCloud" = "轉傳到儲存的訊息"; - -/*Open Pin*/ -"Chat.OpenPin" = "顯示置頂"; -"ChatFilter.Admin" = "管理員"; -"NiceFeatures.Notifications.HideNotifyAccount" = "在通知中隱藏帳號"; -"NiceFeatures.Notifications.HideNotifyAccountNotice" = "而不是「用戶→帳戶」,您將只看到「用戶」。僅適用於多個帳戶。"; -"NiceFeatures.Notifications.Fix" = "停用非必要通知"; -"NiceFeatures.Notifications.FixNotice" = "如果您想將「已關閉通知」的對話完全禁音,這將非常有用。\n來自回覆、@username、媒體預覽的通知將被停用。"; -"NiceFeatures.Filters.Header" = "篩選標籤"; -"NiceFeatures.Filters.Notice" = "選擇篩選標籤的數量。\n長按標籤來更改篩選器。"; -"NiceFeatures.Filters.ShowBadge" = "顯示標記 (小紅點)"; -"NiceFeatures.UseClassicInfoUi" = "使用經典版介面"; - -/*Common*/ -"Common.ExitNow" = "立即結束"; -"Common.Later" = "稍後"; -"Common.RestartRequired" = "必須重新啟動"; -"NiceFeatures.Tabs.ShowNames" = "顯示篩選標籤名稱"; -"Chat.ForwardAsCopy" = "複製後轉傳"; - -/*Folder*/ -"Folder.DefaultName" = "資料夾"; -"Folder.New" = "新資料夾"; -"Folder.Created" = "已建立資料夾"; -"Folder.AddToExisting" = "新增至現有資料夾"; -"Folder.Updated" = "資料夾已更新"; -"Folder.Create" = "建立資料夾…"; -"Folder.Create.Name" = "資料夾名稱"; -"Folder.Create.Placeholder" = "資料夾…"; -"Folder.LimitExceeded" = "抱歉,您最多只能建立 3 個自訂資料夾。\n進階版將可建立更多資料夾。"; -"NiceFeatures.HideNumber" = "在設定中隱藏電話號碼"; - -/*NGWeb*/ -"NGWeb.Blocked" = "由於 App Store 審核指南,在 Nicegram 中不可用"; - -/*Browser*/ -/*Please, Go to «Data & Storage» to configure default browser*/ -"NiceFeatures.Use.DataStorage" = "請使用 «%1» 設定預設瀏覽器"; -"NiceFeatures.Browser.Header" = "連結"; -"NiceFeatures.Browser.UseBrowser" = "在瀏覽器中開啟連結"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram 將在外部瀏覽器開啟連結而不是在 App 內開啟,必須安裝所選擇的瀏覽器。"; -"ChatFilter.Missed" = "錯過的訊息"; - -/*Premium*/ -"Premium.Title" = "進階版功能"; -"Premium.UnlimitedPins.Header" = "無限置頂"; -"Premium.SyncPins" = "同步置頂對話"; -"Premium.SyncPins.Notice.ON" = "如果您在其他客戶端中變更置頂對話,它們在 Nicegram 中將會同步。"; -"Premium.SyncPins.Notice.OFF" = "不如果您在其他客戶端中變更置頂對話,它們在 Nicegram 中將不會同步。"; -"Premium.Missed.Header" = "錯過的訊息"; -"Premium.Missed" = "錯過的訊息通知"; -"Premium.Missed.Notice" = "長時間未上線後開啟 App 時,Nicegram 會通知您未讀的對話及 TAG"; -"Folder.DeleteAsk" = "刪除資料夾"; -"Folder.NeedPremium" = "這個資料夾是進階版功能。您可以透過刷卡來升級,或是刪除一些對話夾。"; -"Common.SupportChatUsername" = "nicegram_tw"; -"Common.FAQUrl" = "https://nicegram-tw.gitbook.io/"; -"Common.FAQ.Button" = "Nicegram 常見問題"; -"Common.FAQ.Intro" = "注意:Nicegram 支援僅由開發人員和志願者提供。\n\n請先看一下 Nicegram 常見問題:它有故障排除指示和其他大多數問題的解答。"; -"IAP.Premium.Title" = "進階版功能"; -"IAP.Premium.Subtitle" = "您無法抗拒的獨特功能!"; -"IAP.Premium.Features" = "訊息快速翻譯"; -"IAP.Premium.Activated" = "已升級進階版!"; -"IAP.Common.Restore" = "恢復購買"; -"IAP.Common.CantPay" = "抱歉,由於您的設備或帳戶限制,您無法購買。"; -"IAP.Common.ErrorFetch" = "抱歉,無法取得 App Store 購買的商品。"; -"IAP.Common.Congrats" = "恭喜!"; -"IAP.Common.ValidateError" = "很抱歉,無法驗證您的購買。"; -"IAP.Common.Connecting" = "正在連接到 AppStore..."; -"Premium.OnetapTranslate" = "快速翻譯按鈕"; -"Premium.IgnoreTranslate.Title" = "忽略的語言"; -"Premium.IgnoreTranslate.Header" = "對於以下選擇的語言,「快速翻譯」按鈕將被停用。"; - -/*Manage Filters*/ -"ManageFilters.Title" = "管理標籤"; -"ManageFilters.Header" = "設定過濾器"; - -"Messages.Translate" = "翻譯"; -"Messages.UndoTranslate" = "取消翻譯"; -"Messages.TranslateError" = "抱歉,無法翻譯。"; - -/*NG Lab Registration Date*/ -"NGLab.RegDate.Btn" = "取得註冊日期"; -"NGLab.RegDate.Title" = "註冊日期"; -"NGLab.RegDate.Notice" = "這是一個大概的日期"; -"NGLab.RegDate.MenuItem" = "已註冊"; -"NGLab.RegDate.FetchError" = "抱歉,無法取得註冊日期。"; -"NGLab.BadDeviceToken" = "抱歉,無法驗證裝置!"; - -/*Backup Settings*/ -"NiceFeatures.BackupIcloud" = "從 iCloud 同步設定與資料夾"; -"NiceFeatures.BackupSettings" = "備份設定與資料夾"; -"NiceFeatures.BackupSettings.Notice" = "建立備份檔,點擊檔案開始還原。"; -"NiceFeatures.BackupSettings.Done" = "備份已儲存"; -"NiceFeatures.BackupSettings.Error" = "建立備份檔時發生錯誤"; - -"NiceFeatures.RestoreSettings.Confirm" = "您確定要從備份檔還原「文件夾和設定」?\n⚠️它將覆蓋當前數據"; -"NiceFeatures.RestoreSettings.Done" = "已成功還原「文件夾與設定」"; -"NiceFeatures.RestoreSettings.Error" = "恢復設定錯誤,備份檔可能已損壞"; - -/*Preview Mode*/ -"Gmod.Restricted" = "您無法在預覽模式下傳送訊息"; -"Gmod.Unavailable" = "預覽模式無法使用"; -"Gmod.Disable.Notice" = "您的「線上狀態」設定將變更為 «%1»"; -"Gmod" = "預覽模式"; -"Gmod.Enable" = "啟用預覽模式?"; -"Gmod.Disable" = "停用預覽模式?"; -"Gmod.Notice" = "您的線上狀態會被 Telegram 隱私設定隱藏。\n如果您進入私人對話,App 會顯示警告。\n如果您傳送或輸入任何訊息,你的線上狀態將可能會被顯示。"; - -"SendWithKb" = "點擊 «Enter» 傳送"; -"NiceFeatures.ShowGmodIcon" = "顯示預覽模式圖示"; -"Gmod.OpenChatQ" = "開啟對話?"; -"Gmod.OpenChatNotice" = "對話將被標記為「已讀」,但您可以長按來預覽聊天。"; -"Gmod.OpenChatBtn" = "是的,打開對話"; -"Gmod.DisableBtn" = "停用警告"; - -"NicegramSettings.Other.hidePhoneInSettingsNotice" = "您的電話號碼只會在介面中隱藏。要完全隱藏起來,請使用「隱私設定」。"; -"NicegramSettings.Other.showProfileId" = "顯示用戶ID"; -"NicegramSettings.Other.showRegDate" = "顯示用戶註冊日期"; -"Premium.OnetapTranslate.LowPower" = "低電量偵測"; -"Premium.OnetapTranslate.LowPower.Notice" = "App 會檢測您設備上每則訊息的語言。這是一項高耗電的功能,會影響您的電池壽命。"; -"Premium.rememberFolderOnExit" = "記下應用程式關閉時所在的資料夾"; diff --git a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings index dca106d3d3f..45df4953416 100644 --- a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings @@ -2,7 +2,8 @@ /*Common*/ "Nicegram.PrivacyPolicy" = "سياسة الخصوصية"; -"Nicegram.EULA" = "اتفاقية ترخيص المستخدم النهائي\n"; +"Nicegram.EULA" = "اتفاقية ترخيص المستخدم النهائي +"; /*ChatFilter*/ "ChatFilter.Bots" = "بوتات"; @@ -19,14 +20,14 @@ "NiceFeatures.Tabs.ShowContacts" = "إظهار تبويب جهات الاتصال"; "NiceFeatures.ChatScreen.Header" = "شاشة الدردشة"; "NiceFeatures.ChatScreen.AnimatedStickers" = "الملصقات المتحركة"; -"NiceFeatures.useBackCam" = "استخدام الكاميرا الخلفية (فيديوهات مستديرة)"; +"NiceFeatures.useBackCam" = "استخدام الكاميرا الخلفية (فيديوهات دائرية)"; "NiceFeatures.Folders.Header" = "المجلدات"; "NiceFeatures.Account.Header" = "إعدادات الحساب"; "NiceFeatures.Folders.TgFolders" = "المجلدات في الأسفل"; "NiceFeatures.Folders.TgFolders.Notice" = "قائمة مجلدات بأسلوب iOS"; -"NiceFeatures.RoundVideos.Header" = "فيديوهات مستديرة"; +"NiceFeatures.RoundVideos.Header" = "فيديوهات دائرية"; "NiceFeatures.RoundVideos.UseRearCamera" = "البدء بالكاميرا الخلفية"; "NiceFeatures.InstantLock" = "فوراً "; @@ -38,9 +39,7 @@ "Chat.OpenPin" = "عرض المثبت"; "ChatFilter.Admin" = "مدير"; "NiceFeatures.Notifications.Fix" = "تعطيل الأشعارات غير المرغوب فيها"; -"NiceFeatures.Notifications.FixNotice" = "مفيد إذا حصلت على إشعارات من دردشات صامتة.\nالرد من الإشعارات، تسميات الحساب، ومعاينات الوسائط لن تكون متاحة."; "NiceFeatures.Filters.Header" = "الفلاتر (التبويبات)"; -"NiceFeatures.Filters.Notice" = "حدد عدد التبويبات المخصصة.\nأنقر مطولًا على التبويب لتغيير الفلتر."; "NiceFeatures.Filters.ShowBadge" = "إظهار الشارات (الفلاتر)"; "NiceFeatures.UseClassicInfoUi" = "استخدام واجهة معلومات الدردشة الكلاسيكية"; @@ -60,7 +59,6 @@ "Folder.Create" = "إنشاء مجلد..."; "Folder.Create.Name" = "إسم المجلد"; "Folder.Create.Placeholder" = "المجلد..."; -"Folder.LimitExceeded" = "عفوًا، لا يمكنك إنشاء أكثر من 3 مجلدات مخصصة.\nالمزيد من المجلدات متوفرة في Premium."; "NiceFeatures.HideNumber" = "إخفاء الرقم من الإعدادات"; /*NGWeb*/ @@ -88,10 +86,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "الأسئلة الشاسعة حول Nicegram"; -"Common.FAQ.Intro" = "يرجى أن يكون في علمك بأن دعم Nicegram يتم بواسطة المطور والمجتمع فقط.\n\nأولًا، ألقى نظرة على الأسئلة الشاسعة بخصوص Nicegram يتضمن نصائح مهمة في استكشاف الأخطاء وإجابات على معظم الأسئلة."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "ميزات فريدة لا يمكنك رفضها!"; -"IAP.Premium.Features" = "مترجم الرسالة السريع\n\nتذكر المجلد المحدد عند الخروج"; "IAP.Premium.Activated" = "تم تفعيل Premium!"; "IAP.Common.Restore" = "استعادة عمليات الشراء"; "IAP.Common.CantPay" = "عفوًا، ولكنك لا تستطيع القيام بعمليات الشراء بسبب قيود في جهازك أو حسابك."; @@ -102,6 +98,7 @@ "Premium.OnetapTranslate" = "زر الترجمة الفوري"; "Premium.IgnoreTranslate.Title" = "اللغات المتجاهلة"; "Premium.IgnoreTranslate.Header" = "زر ترجمة الفوري سوف يعطّل للغات المختارة ادناه."; +"Premium.RecordAllCalls" = "تسجيل جميع المكالمات"; /*Manage Filters*/ "ManageFilters.Title" = "إدارة التبويبات"; @@ -134,7 +131,6 @@ "NiceFeatures.BackupSettings.Done" = "تم إرسال النسح الاحتياطي إلى الرسائل المحفوظة"; "NiceFeatures.BackupSettings.Error" = "خطا في إنشاء نسخة الاحتياط"; -"NiceFeatures.RestoreSettings.Confirm" = "هل أنت متأكد من أنك تودّ استعادة المجلدات والإعدادت من الملف؟\n⚠️سيتم تجاهل البيانات الحالية"; "NiceFeatures.RestoreSettings.Done" = "تمت استعادة المجلدات والإعدادات بنجاح"; "NiceFeatures.RestoreSettings.Error" = "خطأ في استعادة الإعدادت. قد يكون الملف تالفًا"; @@ -145,8 +141,8 @@ "Gmod" = "وضع المعاينة"; "Gmod.Enable" = "تمكين وضع المعاينة؟"; "Gmod.Disable" = "تعطيل وضع المعاينة؟"; -"Gmod.Notice" = "سيتم إخفاء حالة اتصالك بالأنترنت عن الجميع عن طريق إعدادت الخصوصية في تيليجرام.\nسيُظهر التطبيق تحذيرًا إذا كنت تدخل إلى محادثة خاصة.\nقد يتم الكشف عن اتصالك بالأنترنيت إذا قمت بإرسال أو كتابة أي رسالة."; +"ShowNicegramButtonInChat" = "عرض زر Nicegram في الدردشة"; "SendWithKb" = "إرسال مع زر «إدخال»"; "NiceFeatures.ShowGmodIcon" = "إظهار أيقونة وضع المعاينة"; "Gmod.OpenChatQ" = "فتح الدردشة؟"; @@ -188,21 +184,6 @@ "DoubleBottom.Enabled.OK" = "موافق"; "DoubleBottom.Passcode.Error" = "يرجى تعيين رمز مرور مختلف عن الذي تستخدمه في رمز مرور القفل"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "الاستمرار"; -"NicegramOnboarding.1.Title" = "العميل رقم 1 لتيليجرام ماسنجر"; -"NicegramOnboarding.1.Desc" = "انضم إلى أكثر من 2 مليون مستخدم لنايسجرام واحصل على إتاحة الوصول إلى أقوى عميل تيليجرام للأعمال التجارية."; -"NicegramOnboarding.2.Title" = "تجربة مراسلة متقدمة"; -"NicegramOnboarding.2.Desc" = "قم بتسريع اتصالك بميزات فريدة مثل المترجم المُضمن، وتحويل الكلام إلى نصوص، وإعادة توجيه الرسائل دون تحديد المُتحدث، والحفظ السريع في التفضيلات."; -"NicegramOnboarding.3.Title" = "ملف تعريف مستخدم مُمتد"; -"NicegramOnboarding.3.Desc" = "أنشئ أكبر عدد تريده من حسابات تيليجرام مجانًا تمامًا، بالإضافة إلى عرض ملف تعريف مستخدم متقدم مع المعرف، وتاريخ التسجيل، والروابط القابلة للنقر."; -"NicegramOnboarding.4.Title" = "ملحقات الأعمال الفريدة"; -"NicegramOnboarding.4.Desc" = "استفد من الابتكارات من فريق نايسجرام واحصل على eSIM مع إمكانية الوصول إلى الإنترنت في 133 دولة. كما يُمكنك تسجيل حسابات تيليجرام الجديدة على رقم هاتف خطة بيانات eSIM."; -"NicegramOnboarding.5.Title" = "برنامج المراسلة الأكثر أمانًا"; -"NicegramOnboarding.5.Desc" = "برنامج مراسلة قوي بتشفير قوي، ومكالمات صوتية ومرئية جماعية، وقنوات عامة، ومجموعات وبرمجيات بوت، وتخزين سحابي غير محدود للمحادثات، ومشاركة الوسائط والمستندات."; -"NicegramOnboarding.6.Title" = "تعرف على ليلي"; -"NicegramOnboarding.6.Desc" = "مساعد شخصي للتحدث مع الذكاء الاصطناعي وتبسيط حياتك!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "أرسل كفيديو دائري"; "RoundedVideos.MoreButtonTooltip" = "حول الفيديوهات المربعة لترسل كدوائر بنقرة واحدة."; @@ -222,6 +203,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "مشاركة معلومات البوتات"; -"NicegramSettings.ShareChannelsToggle" = "مشاركة معلومات القناة"; +"NicegramSettings.ShareChannelsToggle" = "شارك معلومات القناة"; "NicegramSettings.ShareStickersToggle" = "مشاركة معلومات الملصقات"; "NicegramSettings.ShareData.Note" = "ساهم في أكثر ويكيبيديا شمولية لقنوات ومجموعات تليغرام من خلال إرسال معلومات القناة والرابط تلقائيًا إلى قاعدة بياناتنا. نحن لا نربط ملفك الشخصي في تليغرام والقناة ولا نشارك بياناتك الشخصية."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "تسجيل"; +"NicegramCallRecord.StopAlertTitle" = "إيقاف التسجيل"; +"NicegramCallRecord.StopAlertDescription" = "هل تريد إيقاف تسجيل هذه المكالمة؟"; +"NicegramCallRecord.StopAlertButtonStop" = "توقف"; +"NicegramCallRecord.StopAlertButtonCancel" = "إلغاء الأمر"; +"NicegramCallRecord.SavedMessage" = "تم حفظ المكالمة المسجلة في **الرسائل المحفوظة**."; + +/*Feed*/ +"NicegramFeed.Title" = "فيد"; +"NicegramFeed.Add" = "أضف إلى الخلاصة"; +"NicegramFeed.Remove" = "احذف من الخلاصة"; diff --git a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings index 07cac4b05a3..4a57100abba 100644 --- a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Pin anzeigen"; "ChatFilter.Admin" = "Administrator"; "NiceFeatures.Notifications.Fix" = "Ungewünschte Benachrichtigungen deaktivieren"; -"NiceFeatures.Notifications.FixNotice" = "Nützlich, wenn du Benachrichtigungen von stummgeschalteten Chats erhältst.\nDas Antworten von Benachrichtigungen, Kontobezeichnungen sowie Medienvorschauen werden nicht verfügbar sein."; "NiceFeatures.Filters.Header" = "Filter (Tabs)"; -"NiceFeatures.Filters.Notice" = "Wählen Sie die Anzahl der benutzerdefinierten Tabs.\nLanges Tippen auf Tab, um den Filter zu ändern."; "NiceFeatures.Filters.ShowBadge" = "Abzeichen anzeigen (Filter)"; "NiceFeatures.UseClassicInfoUi" = "Klassische Chat-Info-UI verwenden"; @@ -60,7 +58,6 @@ "Folder.Create" = "Ordner erstellen..."; "Folder.Create.Name" = "Ordnername"; "Folder.Create.Placeholder" = "Ordner..."; -"Folder.LimitExceeded" = "Entschuldigung, Sie können nicht mehr als 3 benutzerdefinierte Ordner erstellen.\nWeitere Ordner sind in Premium verfügbar."; "NiceFeatures.HideNumber" = "Telefon in den Einstellungen ausblenden"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_de"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Fragen und Antworten"; -"Common.FAQ.Intro" = "Bitte beachten Sie, dass der Nicegram Support von einem einzigen Entwickler und der Community durchgeführt wird.\n\nWerfen Sie einen Blick auf die Nicegram-FAQ: Sie enthält wichtige Tipps zur Fehlerbehebung und Antworten auf die meisten Fragen."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Einzigartige Funktionen, die du nicht ablehnen kannst!"; -"IAP.Premium.Features" = "Schnellnachrichten-Übersetzer\n\nAusgewählten Ordner beim Beenden merken"; "IAP.Premium.Activated" = "Premium aktiviert!"; "IAP.Common.Restore" = "Einkäufe wiederherstellen"; "IAP.Common.CantPay" = "Leider können Sie aufgrund Ihres Geräts oder Ihres Kontos keine Einkäufe tätigen."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Schnellübersetzen-Schaltfläche"; "Premium.IgnoreTranslate.Title" = "Ignorierte Sprachen"; "Premium.IgnoreTranslate.Header" = "Die Schaltfläche Schnellnachrichten-Übersetzung wird für die unten ausgewählten Sprachen DEAKTIVIERT sein."; +"Premium.RecordAllCalls" = "Alle Anrufe aufzeichnen"; /*Manage Filters*/ "ManageFilters.Title" = "Tabs verwalten"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Sicherung an gespeicherte Nachrichten gesendet"; "NiceFeatures.BackupSettings.Error" = "Fehler beim Erstellen der Sicherung"; -"NiceFeatures.RestoreSettings.Confirm" = "Sind Sie sicher, dass Sie Ordner & Einstellungen aus der Datei wiederherstellen möchten?\n⚠️ Aktuelle Daten werden überschrieben"; "NiceFeatures.RestoreSettings.Done" = "Ordner & Einstellungen erfolgreich wiederhergestellt"; "NiceFeatures.RestoreSettings.Error" = "Fehler beim Wiederherstellen der Einstellungen. Die Datei könnte beschädigt sein"; @@ -145,8 +140,8 @@ "Gmod" = "Vorschaumodus"; "Gmod.Enable" = "Vorschaumodus aktivieren?"; "Gmod.Disable" = "Vorschaumodus deaktivieren?"; -"Gmod.Notice" = "Dein Onlinestatus wird über die Telegram Privatsphäre-Einstellungen vor jedem versteckt.\nDie App zeigt dir eine Warnung, solltest du einen privaten Chat betreten.\nNachrichten jeglicher Art können deinen Onlinestatus zeigen."; +"ShowNicegramButtonInChat" = "Nicegram-Button im Chat anzeigen"; "SendWithKb" = "Mit «Enter» senden"; "NiceFeatures.ShowGmodIcon" = "Icon für Vorschaumodus anzeigen"; "Gmod.OpenChatQ" = "Chat öffnen?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Bitte legen Sie einen anderen als den für die Passcode-Sperre verwendeten Passcode fest."; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Weiter"; -"NicegramOnboarding.1.Title" = "Nr.1-Client für Telegram-Messenger"; -"NicegramOnboarding.1.Desc" = "Schließen Sie sich über 2 Millionen Nicegram-Nutzern an und erhalten Sie Zugang zum leistungsstärksten und sichersten Telegram-Client für Unternehmen."; -"NicegramOnboarding.2.Title" = "Erweiterte Messaging-Erfahrung"; -"NicegramOnboarding.2.Desc" = "Beschleunigen Sie Ihre Kommunikation mit einzigartigen Funktionen wie einem eingebauten Übersetzer, Speech-2-Text, Weiterleitung von Nachrichten ohne Angabe des Verfassers und Schnellspeicherung in den Favoriten."; -"NicegramOnboarding.3.Title" = "Erweitertes Benutzerprofil"; -"NicegramOnboarding.3.Desc" = "Erstellen Sie kostenlos so viele Telegram-Konten, wie Sie benötigen, und sehen Sie sich ein erweitertes Benutzerprofil mit ID, Registrierungsdatum und klickbaren Links an."; -"NicegramOnboarding.4.Title" = "Einzigartige Geschäftserweiterungen"; -"NicegramOnboarding.4.Desc" = "Profitieren Sie von den Innovationen des Nicegram-Teams und erhalten Sie eine eSIM mit Internetzugang in 133 Ländern. Sie können auch neue Telegram-Konten auf der Telefonnummer des eSIM-Datentarifs registrieren."; -"NicegramOnboarding.5.Title" = "Der sicherste Messenger"; -"NicegramOnboarding.5.Desc" = "Sicherer Messenger mit starker Verschlüsselung, Gruppen-Audio- und Videoanrufe, öffentliche Channels, Gruppen und Bots, unbegrenzter Cloud-Speicher für Chats, Medien- und Dokumentenaustausch."; -"NicegramOnboarding.6.Title" = "Triff Lily"; -"NicegramOnboarding.6.Desc" = "Dein persönlicher KI-Chatbot-Assistent zur Vereinfachung deines Lebens!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Als rundes Video senden"; "RoundedVideos.MoreButtonTooltip" = "Quadratische Videos mit einem Tippen in Kreise umwandeln und senden."; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Teilen Sie Bot-Informationen"; -"NicegramSettings.ShareChannelsToggle" = "Teilen Sie Kanal-Informationen"; +"NicegramSettings.ShareChannelsToggle" = "Kanalinformationen teilen"; "NicegramSettings.ShareStickersToggle" = "Teilen Sie Sticker-Informationen"; "NicegramSettings.ShareData.Note" = "Tragen Sie zur umfassendsten Wikipedia von Telegram-Kanälen und -Gruppen bei, indem Sie automatisch Kanalinformationen und Links zu unserer Datenbank übermitteln. Wir verbinden Ihr Telegram-Profil und Ihren Kanal nicht und teilen Ihre persönlichen Daten nicht."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "aufnehmen"; +"NicegramCallRecord.StopAlertTitle" = "Aufnahme stoppen"; +"NicegramCallRecord.StopAlertDescription" = "Möchten Sie die Aufnahme dieses Anrufs stoppen?"; +"NicegramCallRecord.StopAlertButtonStop" = "Stopp"; +"NicegramCallRecord.StopAlertButtonCancel" = "Abbrechen"; +"NicegramCallRecord.SavedMessage" = "Aufgezeichneter Anruf in **Gespeicherte Nachrichten** gespeichert."; + +/*Feed*/ +"NicegramFeed.Title" = "Feed"; +"NicegramFeed.Add" = "Zum Feed hinzufügen"; +"NicegramFeed.Remove" = "Vom Feed entfernen"; diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d58fadc4681..f2018ecc7d5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -5822,8 +5822,6 @@ Sorry for the inconvenience."; "Conversation.EditingPhotoPanelTitle" = "Edit Photo"; -"Conversation.TextCopied" = "Text copied to clipboard"; - "Media.LimitedAccessTitle" = "Limited Access to Media"; "Media.LimitedAccessText" = "You've given Nicegram access only to select number of photos."; "Media.LimitedAccessManage" = "Manage"; @@ -11129,6 +11127,9 @@ Sorry for the inconvenience."; "Chat.BottomSearchPanel.MessageCount_1" = "message"; "Chat.BottomSearchPanel.MessageCount_any" = "messages"; +"Chat.BottomSearchPanel.StoryCount_1" = "story"; +"Chat.BottomSearchPanel.StoryCount_any" = "stories"; + "Chat.BottomSearchPanel.DisplayModeFormat" = "Show as %@"; "Chat.BottomSearchPanel.DisplayModeChat" = "Chat"; "Chat.BottomSearchPanel.DisplayModeList" = "List"; @@ -11752,9 +11753,11 @@ Sorry for the inconvenience."; "Monetization.TransactionInfo.ViewInExplorer" = "View in Blockchain Explorer"; "Monetization.Intro.Title" = "Earn From Your Channel"; +"Monetization.Intro.Bot.Title" = "Earn From Your Bot"; "Monetization.Intro.Ads.Title" = "Telegram Ads"; "Monetization.Intro.Ads.Text" = "Telegram can display ads in your channel."; +"Monetization.Intro.Bot.Ads.Text" = "Telegram can display ads in your bot."; "Monetization.Intro.Split.Title" = "50:50 Revenue Split"; "Monetization.Intro.Split.Text" = "You receive 50% of the ad revenue in TON."; @@ -11778,13 +11781,18 @@ Sorry for the inconvenience."; "AdsInfo.Info" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:"; "AdsInfo.Respect.Title" = "Respect Your Privacy"; "AdsInfo.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them."; +"AdsInfo.Bot.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the bot in which you see them."; "AdsInfo.Split.Title" = "Help the Channel Creator"; "AdsInfo.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed."; +"AdsInfo.Bot.Split.Title" = "Help the Bot Creator"; +"AdsInfo.Bot.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the bot where they are displayed."; "AdsInfo.Ads.Title" = "Can Be Removed"; "AdsInfo.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium](), and Level %@ channels can remove them for their subscribers."; +"AdsInfo.Bot.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium]()."; "AdsInfo.Launch.Title" = "Can I Launch an Ad?"; "AdsInfo.Launch.Text" = "Anyone can create ads to display in this channel – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"; +"AdsInfo.Bot.Launch.Text" = "Anyone can create ads to display in this bot – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"; "AdsInfo.Launch.Text_URL" = "https://promote.telegram.org"; "AdsInfo.Understood" = "Understood"; @@ -12287,6 +12295,8 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment"; "Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal"; "Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads"; +"Stars.Intro.Transaction.Gift" = "Gift"; +"Stars.Intro.Transaction.ConvertedGift" = "Converted Gift"; "Stars.Intro.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Intro.Transaction.Refund" = "Refund"; @@ -12393,6 +12403,16 @@ Sorry for the inconvenience."; "HashtagSearch.StoriesFound_any" = "%@ Stories Found"; "HashtagSearch.StoriesFoundInfo" = "View stories with %@"; +"HashtagSearch.Stories_1" = "%@ Story"; +"HashtagSearch.Stories_any" = "%@ Stories"; +"HashtagSearch.LocalStoriesFound" = "%1$@ in %2$@"; + +"HashtagSearch.Posts_1" = "%@ Message"; +"HashtagSearch.Posts_any" = "%@ Messages"; +"HashtagSearch.FoundInfoFormat" = "View %1$@ with %2$@"; +"HashtagSearch.FoundStories" = "stories"; +"HashtagSearch.FoundPosts" = "posts"; + "Stars.BotRevenue.Title" = "Stars Balance"; "Stars.BotRevenue.Revenue.Title" = "Stars Received"; "Stars.BotRevenue.Proceeds.Title" = "Rewards Overview"; @@ -12939,3 +12959,245 @@ Sorry for the inconvenience."; "Notification.StarsGiveaway.Subtitle" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is **%2$@**."; "Notification.StarsGiveaway.Subtitle.Stars_1" = "%@ Star"; "Notification.StarsGiveaway.Subtitle.Stars_any" = "%@ Stars"; + +"VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services."; + +"Conversation.CodeCopied" = "Code copied to clipboard"; + +"Stars.Purchase.StarGiftInfo" = "Buy Stars to send **%@** gifts that can be kept on the profile or converted to Stars."; + +"SharedMedia.GiftCount_1" = "%@ gift"; +"SharedMedia.GiftCount_any" = "%@ gifts"; + +"Stars.Info.Title" = "What are Stars?"; +"Stars.Info.Description" = "Telegram Stars allow users to:"; +"Stars.Info.Gift.Title" = "Send Gifts to Friends"; +"Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars."; +"Stars.Info.Miniapp.Title" = "Use Stars in Miniapps"; +"Stars.Info.Miniapp.Text" = "Buy additional content and services in Telegram miniapps. [See Examples >]()"; +"Stars.Info.Media.Title" = "Unlock Content in Channels"; +"Stars.Info.Media.Text" = "Get access to paid content and services in Telegram channels."; +"Stars.Info.Reaction.Title" = "Send Star Reactions"; +"Stars.Info.Reaction.Text" = "Support your favorite channels by sending Star reactions to their posts."; +"Stars.Info.Done" = "Got It"; + +"Gift.View.Title" = "Gift"; +"Gift.View.ReceivedTitle" = "Received Gift"; +"Gift.View.UnavailableTitle" = "Unavailable"; +"Gift.View.KeepOrConvertDescription" = "You can keep this gift in your Profile or convert it to %@. [More About Stars >]()"; +"Gift.View.KeepOrConvertDescription.Stars_1" = "%@ Star"; +"Gift.View.KeepOrConvertDescription.Stars_any" = "%@ Stars"; +"Gift.View.ConvertedDescription" = "You converted this gift to %@. [More About Stars >]()"; +"Gift.View.ConvertedDescription.Stars_1" = "%@ Star"; +"Gift.View.ConvertedDescription.Stars_any" = "%@ Stars"; +"Gift.View.OtherDescription" = "%1$@ can keep this gift in their Profile or convert it to %2$@. [More About Stars >]()"; +"Gift.View.OtherDescription.Stars_1" = "%@ Star"; +"Gift.View.OtherDescription.Stars_any" = "%@ Stars"; +"Gift.View.UnavailableDescription" = "This gift has sold out"; +"Gift.View.From" = "From"; +"Gift.View.HiddenName" = "Hidden Name"; +"Gift.View.Send" = "send a gift"; +"Gift.View.Date" = "Date"; +"Gift.View.FirstSale" = "First Sale"; +"Gift.View.LastSale" = "Last Sale"; +"Gift.View.Value" = "Value"; +"Gift.View.Sale" = "sell for %@"; +"Gift.View.Sale.Stars_1" = "%@ Star"; +"Gift.View.Sale.Stars_any" = "%@ Stars"; +"Gift.View.Availability" = "Availability"; +"Gift.View.Availability.Of" = "%1$@ of %2$@"; +"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left"; +"Gift.View.Visibility" = "Visibility"; +"Gift.View.Visibility.Visible" = "Visible on your page"; +"Gift.View.Visibility.Hide" = "hide"; +"Gift.View.Hide" = "Hide from My Page"; +"Gift.View.Display" = "Display on My Page"; +"Gift.View.Convert" = "Convert to %@"; +"Gift.View.Convert.Stars_1" = "%@ Star"; +"Gift.View.Convert.Stars_any" = "%@ Stars"; + +"Gift.View.NameHidden" = "Only you can see the senders's name."; +"Gift.View.NameAndMessageHidden" = "Only you can see the senders's name and message."; + +"Gift.View.DisplayedInfo" = "The gift is visible on your Page. [View >]()"; +"Gift.View.HiddenInfo" = "This gift is hidden. Only you can see it."; + +"Gift.Displayed.Title" = "Gift Saved to Profile"; +"Gift.Displayed.Text" = "The gift is now displayed in [your profile]()."; +"Gift.Displayed.NewText" = "The gift is now shown on your Page."; +"Gift.Displayed.View" = "View"; +"Gift.Hidden.Title" = "Gift Removed from Profile"; +"Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]()."; +"Gift.Hidden.NewText" = "The gift is removed from your Page."; +"Gift.Convert.Title" = "Convert Gift to Stars"; +"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift."; +"Gift.Convert.Stars_1" = "%@ Star"; +"Gift.Convert.Stars_any" = "%@ Stars"; +"Gift.Convert.Convert" = "Convert"; +"Gift.Convert.Success.Title" = "Gift Converted"; +"Gift.Convert.Success.Text" = "You received **%1$@** instead."; +"Gift.Convert.Success.Text.Stars_1" = "%@ Star"; +"Gift.Convert.Success.Text.Stars_any" = "%@ Stars"; + +"Gift.Options.Premium.Title" = "Gift Premium"; +"Gift.Options.Premium.Text" = "Give **%@** access to exclusive features with Telegram Premium. [See Features >]()"; +"Gift.Options.Premium.Months_1" = "%@ month"; +"Gift.Options.Premium.Months_any" = "%@ months"; +"Gift.Options.Premium.Years_1" = "%@ year"; +"Gift.Options.Premium.Years_any" = "%@ years"; +"Gift.Options.Premium.Premium" = "Premium"; +"Gift.Options.Gift.Title" = "Send a Gift"; +"Gift.Options.Gift.Text" = "Give **%@** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()"; +"Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; +"Gift.Options.Gift.Filter.Limited" = "Limited"; +"Gift.Options.Gift.Limited" = "limited"; +"Gift.Options.Gift.SoldOut" = "sold out"; +"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out."; + +"PeerInfo.PaneGifts" = "Gifts"; + +"PeerInfo.Gifts.Info" = "These gifts were sent to you by other users. Tap on a gift to exchange it for Stars or change its privacy settings."; +"PeerInfo.Gifts.Send" = "Send Gifts to Friends"; +"PeerInfo.Gifts.OneOf" = "1 of %@"; + +"Notification.StarGift.Title" = "Gift from %@"; +"Notification.StarGift.Subtitle" = "Display this gift on your page or convert it to %@."; +"Notification.StarGift.Subtitle.Stars_1" = "%@ Star"; +"Notification.StarGift.Subtitle.Stars_any" = "%@ Stars"; +"Notification.StarGift.Subtitle.Other" = "%1$@ can keep this gift on their page or convert it to %2$@."; +"Notification.StarGift.Subtitle.Other.Stars_1" = "%@ Star"; +"Notification.StarGift.Subtitle.Other.Stars_any" = "%@ Stars"; +"Notification.StarGift.Subtitle.Converted" = "You converted this gift to %@."; +"Notification.StarGift.Subtitle.Converted.Stars_1" = "%@ Star"; +"Notification.StarGift.Subtitle.Converted.Stars_any" = "%@ Stars"; +"Notification.StarGift.Subtitle.Displaying" = "You are displaying this gift on your page. You can also convert it to %@."; +"Notification.StarGift.Subtitle.Displaying.Stars_1" = "%@ Star"; +"Notification.StarGift.Subtitle.Displaying.Stars_any" = "%@ Stars"; +"Notification.StarGift.OneOf" = "1 of %@"; +"Notification.StarGift.View" = "View"; + +"Gift.Send.Title" = "Send a Gift"; +"Gift.Send.Customize.Title" = "CUSTOMIZE YOUR GIFT"; +"Gift.Send.Customize.MessagePlaceholder" = "Enter Message (Optional)"; +"Gift.Send.Customize.Info" = "Only %@ will see your message."; +"Gift.Send.HideMyName" = "Hide My Name"; +"Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; +"Gift.Send.Send" = "Send a Gift for"; +"Gift.Send.Limited" = "Limited"; +"Gift.Send.Remains_1" = "%@ left"; +"Gift.Send.Remains_any" = "%@ left"; + +"Gift.Send.ErrorUnknown" = "An error occurred. Please try again."; +"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift."; + +"Profile.SendGift" = "Send a Gift"; +"Settings.SendGift" = "Send a Gift"; + +"Gift.PremiumOrStars.Title" = "Gift Premium or Stars"; + +"Report.Title.Message" = "Report Message"; +"Report.Title.Story" = "Report Story"; +"Report.Title.User" = "Report User"; +"Report.Title.Group" = "Report Group"; +"Report.Title.Channel" = "Report Channel"; +"Report.Title.Bot" = "Report Bot"; +"Report.Comment.Placeholder" = "Add Comment"; +"Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; +"Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; +"Report.Send" = "Send Report"; + +"Notification.PremiumGift.MonthsTitle_1" = "%@ Month Premium"; +"Notification.PremiumGift.MonthsTitle_any" = "%@ Months Premium"; + +"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; +"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; +"Notification.PremiumGift.More" = "more"; + +"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; + +"Notification.StarsGift.Stars_1" = "%@ Star"; +"Notification.StarsGift.Stars_any" = "%@ Stars"; + +"WebBrowser.AuthChallenge.Title" = "Sign in to %@"; +"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; + + +"ChatList.Search.FilterPublicPosts" = "Public Posts"; +"DialogList.SearchSectionPublicPosts" = "Public Posts"; + +"Chat.PrivateMessageEditTimestamp.Date" = "edited %@"; +"Chat.PrivateMessageEditTimestamp.TodayAt" = "edited today at %@"; +"Chat.PrivateMessageEditTimestamp.YesterdayAt" = "edited yesterday at %@"; + +"Stars.Transaction.Gift.Title" = "Gift"; + +"ChatList.Search.Open" = "OPEN"; +"ChatList.Search.ShowMore" = "Show More"; + +"AttachmentMenu.AddPhotoOrVideo" = "Add Photo or Video"; +"AttachmentMenu.AddDocument" = "Add Document"; + +"Chat.BotAd.Title" = "Ad"; +"Chat.BotAd.WhatIsThis" = "what's this?"; + +"ChatList.Search.TopAppsInfo" = "Which apps are included here? [Learn >]()"; + +"TopApps.Info.Title" = "Top Mini Apps"; +"TopApps.Info.Text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini app in [@botfather]() (as described [here](https://core.telegram.org/bots/webapps#launching-the-main-mini-app)), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on weekly average."; +"TopApps.Info.Done" = "Understood"; + +"Stars.Intro.Transaction.TelegramBotApi.Title" = "Paid Broadcast"; +"Stars.Intro.Transaction.TelegramBotApi.Messages_1" = "%@ Message"; +"Stars.Intro.Transaction.TelegramBotApi.Messages_any" = "%@ Messages"; + +"Stars.Transaction.TelegramBotApi.Title" = "Paid Broadcast"; +"Stars.Transaction.TelegramBotApi.Messages_1" = "%@ Message"; +"Stars.Transaction.TelegramBotApi.Messages_any" = "%@ Messages"; + +"Monetization.Bot.Header" = "Telegram shares 50% of the revenue from ads displayed in your bot. [Learn More >]()"; +"Monetization.Bot.BalanceTitle" = "AVAILABLE BALANCE"; + +"Resolve.ChannelErrorNotFound" = "Sorry, this channel doesn't seem to exist."; + +"Stats.TonBotRevenue.Title" = "TON Balance"; + +"PeerInfo.BotBalance.Title" = "BALANCE"; +"PeerInfo.BotBalance.Ton" = "Ton"; +"PeerInfo.BotBalance.Stars" = "Stars"; + +"Gallery.ToastVideoSpeedSwipe" = "Swipe sideways to adjust speed."; +"Gallery.VideoSettings.QualitySectionTitle" = "QUALITY"; +"Gallery.VideoSettings.SpeedSectionTitle" = "PLAYBACK SPEED"; +"Gallery.VideoSettings.SpeedControlTitle" = "Speed"; + +"Gallery.VideoSettings.QualityAuto" = "Auto"; +"Gallery.VideoSettings.QualityLow" = "Low"; +"Gallery.VideoSettings.QualityMedium" = "Medium"; +"Gallery.VideoSettings.QualityHD" = "High"; +"Gallery.VideoSettings.QualityFHD" = "Full HD"; +"Gallery.VideoSettings.QualityQHD" = "Ultra-High"; + +"Gallery.VideoSettings.IconQualityLow" = "L"; +"Gallery.VideoSettings.IconQualityMedium" = "SD"; +"Gallery.VideoSettings.IconQualityHD" = "HD"; +"Gallery.VideoSettings.IconQualityFHD" = "FHD"; +"Gallery.VideoSettings.IconQualityQHD" = "QHD"; + +"Gallery.MenuSaveToGallery" = "Save to Gallery"; +"Gallery.SaveToGallery.Quality" = "Save in %@p"; +"Gallery.SaveToGallery.Original" = "Save Original"; + +"VideoChat.ScheduleButtonTitle" = "Schedule Video Chat"; + +"Chat.ToastImprovingVideo.Title" = "Improving video..."; +"Chat.ToastImprovingVideo.Text" = "The video will be published after it's optimized for the best viewing experience."; +"Chat.ToastVideoPublished.Title" = "Video Published"; +"Chat.ToastVideoPublished.Action" = "View"; +"Chat.MessageTooltipVideoProcessing" = "Processing video may take a few minutes."; + +"Chat.VideoProcessingServiceMessage_1" = "This video will be published once converted and optimized"; +"Chat.VideoProcessingServiceMessage_any" = "These videos will be published once converted and optimized"; + +"Chat.ScheduledForceSendProcessingVideo.Title" = "Wait!"; +"Chat.ScheduledForceSendProcessingVideo.Text" = "This video hasn't been converted and optimized yet. If you send it now, the viewers of the video may experience slow download speed."; +"Chat.ScheduledForceSendProcessingVideo.Action" = "Send Anyway"; diff --git a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings index 13f9bede3e9..98e9ae4bd10 100644 --- a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings @@ -97,6 +97,7 @@ "Premium.OnetapTranslate" = "Quick Translate button"; "Premium.IgnoreTranslate.Title" = "Ignored Languages"; "Premium.IgnoreTranslate.Header" = "Quick Translate button will be DISABLED for languages you choose below."; +"Premium.RecordAllCalls" = "Record all calls"; /*Manage Filters*/ "ManageFilters.Title" = "Manage Tabs"; @@ -204,3 +205,16 @@ "NicegramSettings.ShareChannelsToggle" = "Share channel information"; "NicegramSettings.ShareStickersToggle" = "Share stickers information"; "NicegramSettings.ShareData.Note" = "Contribute to the most comprehensive wikipedia of Telegram channels and groups by automatically submitting channel information and link to our database. We do not connect your telegram profile and channel and do not share your personal data."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "record"; +"NicegramCallRecord.StopAlertTitle" = "Stop recording"; +"NicegramCallRecord.StopAlertDescription" = "Do you want to stop recording this call?"; +"NicegramCallRecord.StopAlertButtonStop" = "Stop"; +"NicegramCallRecord.StopAlertButtonCancel" = "Cancel"; +"NicegramCallRecord.SavedMessage" = "Recorded Call saved to **Saved Messages**."; + +/*Feed*/ +"NicegramFeed.Title" = "Feed"; +"NicegramFeed.Add" = "Add to Feed"; +"NicegramFeed.Remove" = "Remove from Feed"; diff --git a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings index cbf6e73784d..5522d746d39 100644 --- a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Mostrar anclado"; "ChatFilter.Admin" = "Administrador"; "NiceFeatures.Notifications.Fix" = "Desactivar notificaciones no deseadas"; -"NiceFeatures.Notifications.FixNotice" = "Útil si recibes notificaciones de chats silenciados.\nResponder desde la notificación, etiquetas de cuenta y vistas previas de multimedia no estarán disponibles."; "NiceFeatures.Filters.Header" = "FILTROS (PESTAÑAS)"; -"NiceFeatures.Filters.Notice" = "Selecciona el número de pestañas personalizadas. \nMantén pulsado una pestaña para cambiar el filtro."; "NiceFeatures.Filters.ShowBadge" = "Mostrar globos (Filtros)"; "NiceFeatures.UseClassicInfoUi" = "Usar interfaz clásica de perfil"; @@ -60,7 +58,6 @@ "Folder.Create" = "Crear carpeta…"; "Folder.Create.Name" = "Nombre de la carpeta"; "Folder.Create.Placeholder" = "Carpeta…"; -"Folder.LimitExceeded" = "Lo siento pero no se puede crear más que 3 archivos personalizados.\nHay más archivos disponibles en Premium."; "NiceFeatures.HideNumber" = "Ocultar número en Ajustes"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_es"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Preguntas frecuentes de Nicegram"; -"Common.FAQ.Intro" = "Ten en cuenta que el soporte de Nicegram sólo lo realiza un desarrollador y una comunidad.\n\nPrimero, echa un vistazo a las preguntas frecuentes de Nicegram: Tiene consejos importantes para la solución de la mayoría de problemas."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "¡Características únicas que no puedes rechazar!"; -"IAP.Premium.Features" = "Traductor rápido de mensajes\n\nRecordar la carpeta seleccionada a la salida"; "IAP.Premium.Activated" = "¡Premium activado!"; "IAP.Common.Restore" = "Restaurar compras"; "IAP.Common.CantPay" = "Lo sentimos, pero no se puede realizar compras debido a tu dispositivo o las restricciones de tu cuenta."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Botón de traducción rápida"; "Premium.IgnoreTranslate.Title" = "Idiomas ignorados"; "Premium.IgnoreTranslate.Header" = "El botón de traducción rápida será DESACTIVADO para los idiomas que elijas debajo."; +"Premium.RecordAllCalls" = "Grabar todas las llamadas"; /*Manage Filters*/ "ManageFilters.Title" = "Administrar pestañas"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Copia de seguridad hecha en Mensajes guardados"; "NiceFeatures.BackupSettings.Error" = "Error al crear la copia de seguridad"; -"NiceFeatures.RestoreSettings.Confirm" = "¿Quieres restaurar las carpetas y ajustes desde un archivo?\n⚠️ Sobreescribirá los datos actuales"; "NiceFeatures.RestoreSettings.Done" = "Carpetas y ajustes restaurados con éxito"; "NiceFeatures.RestoreSettings.Error" = "Error al restaurar los ajustes. El archivo podría estar corrupto"; @@ -145,8 +140,8 @@ "Gmod" = "Modo de vista previa"; "Gmod.Enable" = "¿Activar modo vista previa?"; "Gmod.Disable" = "¿Desactivar modo vista previa?"; -"Gmod.Notice" = "Tu estado en línea estará oculto para todos por la configuración de privacidad de Telegram.\nLa aplicación mostrará una advertencia si estás entrando en un chat privado.\nTu estado en línea puede ser revelado si envías o escribes CUALQUIER mensaje."; +"ShowNicegramButtonInChat" = "Mostrar botón Nicegram en el chat"; "SendWithKb" = "Envía con el botón «Intro»"; "NiceFeatures.ShowGmodIcon" = "Mostrar icono de modo vista previa"; "Gmod.OpenChatQ" = "¿Abrir chat?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "Vale"; "DoubleBottom.Passcode.Error" = "Establezca otro código de acceso diferente del que usa para «Passcode Lock»"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Continuar"; -"NicegramOnboarding.1.Title" = "Cliente nº 1 de Telegram Messenger"; -"NicegramOnboarding.1.Desc" = "Únase a más de 2 millones de usuarios de Nicegram y acceda al cliente de Telegram más potente y seguro para empresas."; -"NicegramOnboarding.2.Title" = "Experiencia de mensajería avanzada"; -"NicegramOnboarding.2.Desc" = "Agilice sus comunicaciones con funciones únicas como el traductor integrado, el sistema de voz a texto, el reenvío de mensajes sin indicar el autor y el guardado rápido en favoritos."; -"NicegramOnboarding.3.Title" = "Perfil de usuario ampliado"; -"NicegramOnboarding.3.Desc" = "Cree tantas cuentas de Telegram como necesite de forma totalmente gratuita, y vea un perfil de usuario avanzado con ID, fecha de registro y enlaces clicables."; -"NicegramOnboarding.4.Title" = "Extensiones comerciales únicas"; -"NicegramOnboarding.4.Desc" = "Aproveche las innovaciones del equipo de Nicegram y obtenga una eSIM con acceso a Internet en 133 países. También puede registrar nuevas cuentas de Telegram en el número de teléfono del plan de datos eSIM."; -"NicegramOnboarding.5.Title" = "El servicio de mensajería más seguro"; -"NicegramOnboarding.5.Desc" = "Servicio de mensajería seguro con cifrado robusto, llamadas de audio y vídeo grupales, canales públicos, grupos y \"bots\", almacenamiento ilimitado en la nube para chats, multimedia y documentos compartidos."; -"NicegramOnboarding.6.Title" = "Conoce a Lily"; -"NicegramOnboarding.6.Desc" = "¡Tu chatbot de IA - el asistente personal diseñado para simplificar tu vida!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Enviar como Vídeo Redondeado"; "RoundedVideos.MoreButtonTooltip" = "Convierte videos cuadrados en círculos para enviar con un solo toque."; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Compartir información de bots"; -"NicegramSettings.ShareChannelsToggle" = "Compartir información de canales"; +"NicegramSettings.ShareChannelsToggle" = "Compartir información del canal"; "NicegramSettings.ShareStickersToggle" = "Compartir información de stickers"; "NicegramSettings.ShareData.Note" = "Contribuye a la wikipedia más completa de canales y grupos de Telegram enviando automáticamente información y enlaces de canales a nuestra base de datos. No conectamos tu perfil de telegram con el canal y no compartimos tus datos personales."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "grabar"; +"NicegramCallRecord.StopAlertTitle" = "Detener grabación"; +"NicegramCallRecord.StopAlertDescription" = "¿Quieres detener la grabación de esta llamada?"; +"NicegramCallRecord.StopAlertButtonStop" = "Detener"; +"NicegramCallRecord.StopAlertButtonCancel" = "Cancelar"; +"NicegramCallRecord.SavedMessage" = "Llamada grabada guardada en **Mensajes Guardados**."; + +/*Feed*/ +"NicegramFeed.Title" = "Feed"; +"NicegramFeed.Add" = "Añadir al feed"; +"NicegramFeed.Remove" = "Eliminar del feed"; diff --git a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings index cc9d348f72d..8992b083c1c 100644 --- a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Afficher l'épingle"; "ChatFilter.Admin" = "Administrateur"; "NiceFeatures.Notifications.Fix" = "Désactiver les notifications indésirables"; -"NiceFeatures.Notifications.FixNotice" = "Utile si vous recevez des notifications de chats en sourdine.\nLes réponses à partir des notifications, des libellés de compte et des aperçus multimédias ne seront pas disponibles."; "NiceFeatures.Filters.Header" = "FILTRES (ONGLETS)"; -"NiceFeatures.Filters.Notice" = "Sélectionnez le nombre d'onglets personnalisés.\nAppuyez longuement sur l'onglet pour modifier le filtre."; "NiceFeatures.Filters.ShowBadge" = "Afficher les badges (filtres)"; "NiceFeatures.UseClassicInfoUi" = "Utiliser l'interface utilisateur classique d'informations sur le chat"; @@ -60,7 +58,6 @@ "Folder.Create" = "Créer le dossier..."; "Folder.Create.Name" = "Nom du dossier"; "Folder.Create.Placeholder" = "Dossier..."; -"Folder.LimitExceeded" = "Désolé, vous ne pouvez pas créer plus de trois dossiers personnalisés.\nPlus de dossiers sont disponibles dans Premium."; "NiceFeatures.HideNumber" = "Masquer le téléphone dans les paramètres"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "FAQ Nicegram"; -"Common.FAQ.Intro" = "Veuillez noter que le support Nicegram est assuré par le seul développeur et communauté.\n\nTout d’abord, jetez un œil à la FAQ Nicegram : elle contient des conseils de dépannage importants et des réponses à la plupart des questions."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Des fonctionnalités uniques que vous ne pouvez pas refuser !"; -"IAP.Premium.Features" = "Traducteur de messages rapides\n\nMémoriser le dossier sélectionné à la sortie"; "IAP.Premium.Activated" = "Premium activé !"; "IAP.Common.Restore" = "Restaurer les achats"; "IAP.Common.CantPay" = "Désolé, mais vous ne pouvez pas effectuer d'achats en raison des restrictions de votre appareil ou de votre compte."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Bouton de traduction rapide"; "Premium.IgnoreTranslate.Title" = "Langues ignorées"; "Premium.IgnoreTranslate.Header" = "Le bouton de traduction rapide sera DÉSACTIVÉ pour les langues que vous choisissez ci-dessous."; +"Premium.RecordAllCalls" = "Enregistrer tous les appels"; /*Manage Filters*/ "ManageFilters.Title" = "Gérer les onglets"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Sauvegarde envoyée aux messages sauvegardés"; "NiceFeatures.BackupSettings.Error" = "Erreur lors de la création de la sauvegarde"; -"NiceFeatures.RestoreSettings.Confirm" = "Voulez-vous vraiment restaurer les dossiers et paramètres à partir du fichier ?\n⚠️ Cela remplacera les données actuelles"; "NiceFeatures.RestoreSettings.Done" = "Dossiers et paramètres restaurés avec succès"; "NiceFeatures.RestoreSettings.Error" = "Erreur lors de la restauration des paramètres. Il se peut que le fichier soit corrompu"; @@ -145,8 +140,8 @@ "Gmod" = "Mode « Aperçu »"; "Gmod.Enable" = "Activer le mode « Aperçu » ?"; "Gmod.Disable" = "Désactiver le mode « Aperçu » ?"; -"Gmod.Notice" = "Votre statut en ligne sera masqué pour tout le monde par les paramètres de confidentialité de Telegram.\nL'application affichera un avertissement si vous entrez dans un chat privé.\nVotre statut en ligne peut être révélé si vous envoyez ou tapez N'IMPORTE QUEL message."; +"ShowNicegramButtonInChat" = "Afficher le bouton Nicegram dans le chat"; "SendWithKb" = "Envoyer avec le bouton « Entrer »"; "NiceFeatures.ShowGmodIcon" = "Afficher l'icône du mode « Aperçu »"; "Gmod.OpenChatQ" = "Ouvrir le chat ?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "D'ACCORD"; "DoubleBottom.Passcode.Error" = "Veuillez définir un autre code d'accès différent de celui que vous utilisez pour le verrouillage par code d'accès"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Continuer"; -"NicegramOnboarding.1.Title" = "Le client nº 1 pour Telegram Messenger"; -"NicegramOnboarding.1.Desc" = "Rejoignez plus de 2 millions d'utilisateurs Nicegram et accédez au client Telegram le plus puissant et le plus sécurisé pour les entreprises."; -"NicegramOnboarding.2.Title" = "Expérience de messagerie avancée"; -"NicegramOnboarding.2.Desc" = "Accélérez votre communication avec des fonctionnalités uniques telles qu'un Traducteur intégré, Speech 2 Text, le Transfert de messages sans spécifier l'auteur et l'Enregistrement rapide dans les favoris."; -"NicegramOnboarding.3.Title" = "Profil utilisateur étendu"; -"NicegramOnboarding.3.Desc" = "Créez gratuitement autant de comptes Telegram que vous le souhaitez et visualisez un profil d'utilisateur avancé avec identifiant, date d'enregistrement et liens cliquables."; -"NicegramOnboarding.4.Title" = "Extensions commerciales uniques"; -"NicegramOnboarding.4.Desc" = "Profitez des innovations de l'équipe Nicegram et obtenez une eSIM avec un accès internet dans 133 pays. Vous pouvez également enregistrer de nouveaux comptes Telegram sur le numéro de téléphone du forfait de données eSIM."; -"NicegramOnboarding.5.Title" = "La messagerie la plus sécurisée"; -"NicegramOnboarding.5.Desc" = "Messagerie sécurisée avec cryptage fort, appels audio et vidéo de groupe, chaînes publiques, groupes et bots, stockage illimité dans le cloud pour les discussions, partage de médias et de documents."; -"NicegramOnboarding.6.Title" = "Rencontrer Lily"; -"NicegramOnboarding.6.Desc" = "Votre assistant personnel AI Chatbot pour simplifier votre vie!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Envoyer en tant que vidéo arrondie"; "RoundedVideos.MoreButtonTooltip" = "Convertissez des vidéos carrées en cercles à envoyer en un seul clic."; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Partager les informations des bots"; -"NicegramSettings.ShareChannelsToggle" = "Partager les informations du canal"; +"NicegramSettings.ShareChannelsToggle" = "Partager les informations de la chaîne"; "NicegramSettings.ShareStickersToggle" = "Partager des informations sur les stickers"; "NicegramSettings.ShareData.Note" = "Contribuez au Wikipédia le plus complet de chaînes et de groupes Telegram en soumettant automatiquement des informations sur les chaînes et un lien vers notre base de données. Nous ne connectons pas votre profil de télégramme et votre canal et ne partageons pas vos données personnelles."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "enregistrer"; +"NicegramCallRecord.StopAlertTitle" = "Arrêter l'enregistrement"; +"NicegramCallRecord.StopAlertDescription" = "Voulez-vous arrêter d'enregistrer cet appel ?"; +"NicegramCallRecord.StopAlertButtonStop" = "Arrêter"; +"NicegramCallRecord.StopAlertButtonCancel" = "Annuler"; +"NicegramCallRecord.SavedMessage" = "Appel enregistré sauvegardé dans **Messages Sauvegardés**."; + +/*Feed*/ +"NicegramFeed.Title" = "Flux"; +"NicegramFeed.Add" = "Ajouter au flux"; +"NicegramFeed.Remove" = "Retirer du flux"; diff --git a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings index ebf6aef7acc..9c63e7946b6 100644 --- a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Mostra pin"; "ChatFilter.Admin" = "Amministratore"; "NiceFeatures.Notifications.Fix" = "Disabilita notifiche indesiderate"; -"NiceFeatures.Notifications.FixNotice" = "Utile se ricevi notifiche da chat silenziate.\nLe risposte alle notifiche, etichette degli account e le anteprime multimediali non saranno disponibili."; "NiceFeatures.Filters.Header" = "FILTRI (TAB)"; -"NiceFeatures.Filters.Notice" = "Seleziona il numero di tab personalizzate.\nTocca a lungo la tab per cambiare filtro."; "NiceFeatures.Filters.ShowBadge" = "Mostra badge (Filtri)"; "NiceFeatures.UseClassicInfoUi" = "Usa l'interfaccia delle info classica"; @@ -60,7 +58,6 @@ "Folder.Create" = "Crea cartella..."; "Folder.Create.Name" = "Nome cartella"; "Folder.Create.Placeholder" = "Cartella..."; -"Folder.LimitExceeded" = "Spiacente, puoi creare solo 3 cartelle personalizzate.\nPuoi aggiungerne di più con Premium."; "NiceFeatures.HideNumber" = "Nascondi il numero di telefono nelle impostazioni"; /*NGWeb*/ @@ -71,7 +68,8 @@ "NiceFeatures.Use.DataStorage" = "Per favore, usa «%1» per configurare il tuo browser predefinito"; "NiceFeatures.Browser.Header" = "URL"; "NiceFeatures.Browser.UseBrowser" = "Apri link nel browser"; -"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram aprirà i link in un browser esterno invece di farlo nell'app.\nIl browser selezionato deve essere installato."; +"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram aprirà i link in un browser esterno invece di farlo nell'app. +Il browser selezionato deve essere installato."; "ChatFilter.Missed" = "Mancati"; /*Premium*/ @@ -88,10 +86,8 @@ "Common.SupportChatUsername" = "nicegram_it"; "Common.FAQUrl" = "https://nicegram.app/it/faq/"; "Common.FAQ.Button" = "FAQ Nicegram"; -"Common.FAQ.Intro" = "Per favore, nota che il supporto di Nicegram è operato solo dall'unico sviluppatore e dalla community.\n\nPrima di tutto, dai una occhiata alle FAQ di Nicegram: contiene molte soluzioni a problemi frequenti."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Feature uniche che non puoi rifiutare!"; -"IAP.Premium.Features" = "Traduttore istantaneo dei messaggi\n\nRicorda la cartella corrente all'uscita"; "IAP.Premium.Activated" = "Premium attivato!"; "IAP.Common.Restore" = "Ripristina acquisti"; "IAP.Common.CantPay" = "Spiacente, non puoi effettuare acquisti a causa di restrizioni sul tuo dispositivo o account."; @@ -102,6 +98,7 @@ "Premium.OnetapTranslate" = "Bottone di traduzione rapida"; "Premium.IgnoreTranslate.Title" = "Lingue ignorate"; "Premium.IgnoreTranslate.Header" = "Il bottone di traduzione rapida sarà DISABILITATO per le lingue che selezioni sotto."; +"Premium.RecordAllCalls" = "Registra tutte le chiamate"; /*Manage Filters*/ "ManageFilters.Title" = "Gestisci tab"; @@ -134,7 +131,6 @@ "NiceFeatures.BackupSettings.Done" = "Backup eseguito nei messaggi salvati"; "NiceFeatures.BackupSettings.Error" = "Errore creazione backup"; -"NiceFeatures.RestoreSettings.Confirm" = "Sicuro di voler ripristinare cartelle e impostazioni dal file?\n⚠️ Questo sovrascriverà i dati attuali"; "NiceFeatures.RestoreSettings.Done" = "✔️ Cartelle e impostazioni ripristinate"; "NiceFeatures.RestoreSettings.Error" = "Errore ripristino impostazioni. Il file potrebbe essere corrotto"; @@ -145,8 +141,8 @@ "Gmod" = "Modalità anteprima"; "Gmod.Enable" = "Abilitare modalità anteprima?"; "Gmod.Disable" = "Disabilitare modalità anteprima?"; -"Gmod.Notice" = "Il tuo stato online sarà nascosto da tutti tramite le impostazioni della privacy di Telegram.\nL'app mostrerà un avviso se entri in una chat privata.\nIl tuo stato online verrà rivelato se mandi o scrivi qualunque messaggio."; +"ShowNicegramButtonInChat" = "Mostra il pulsante Nicegram nella chat"; "SendWithKb" = "Invia con il pulsante «Invio»"; "NiceFeatures.ShowGmodIcon" = "Mostra icona modalità anteprima"; "Gmod.OpenChatQ" = "Aprire chat?"; @@ -188,21 +184,6 @@ "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Si prega di impostare un altro codice di accesso diverso da quello utilizzato per il blocco del codice stesso"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Continua"; -"NicegramOnboarding.1.Title" = "Il client numero uno per Telegram"; -"NicegramOnboarding.1.Desc" = "Unisciti a oltre 2 milioni di utenti Nicegram e accedi al client Telegram più potente e sicuro per le aziende."; -"NicegramOnboarding.2.Title" = "Esperienza di messaggistica avanzata"; -"NicegramOnboarding.2.Desc" = "Velocizza la tua comunicazione con funzionalità uniche come un traduttore integrato, riconoscimento vocale, inoltro di messaggi senza indicare l'autore e salvataggio rapido nei preferiti."; -"NicegramOnboarding.3.Title" = "Profilo utente esteso"; -"NicegramOnboarding.3.Desc" = "Crea tutti gli account Telegram di cui hai bisogno in modo completamente gratuito, oltre a visualizzare un profilo utente avanzato con ID, data di registrazione e link cliccabili."; -"NicegramOnboarding.4.Title" = "Estensioni aziendali uniche"; -"NicegramOnboarding.4.Desc" = "Approfitta delle innovazioni del team di Nicegram e ottieni una eSIM con accesso a Internet in 133 paesi. Puoi inoltre registrare nuovi account Telegram al numero di telefono del piano dati della eSIM."; -"NicegramOnboarding.5.Title" = "Il client di messaggistica più sicuro"; -"NicegramOnboarding.5.Desc" = "Client di messaggistica sicuro con crittografia avanzata, chiamate audio e video di gruppo, canali pubblici, gruppi e bot, spazio di archiviazione cloud illimitato per le chat, condivisione di file multimediali e documenti."; -"NicegramOnboarding.6.Title" = "Conosci Lily"; -"NicegramOnboarding.6.Desc" = "Il tuo assistente personale Chatbot d'IA per semplificare la tua vita!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Invia come video rotondo"; "RoundedVideos.MoreButtonTooltip" = "Converti i video quadrati in cerchi da inviare con un solo tocco."; @@ -222,6 +203,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Condividi informazioni sui bot"; -"NicegramSettings.ShareChannelsToggle" = "Condividi informazioni sul canale"; +"NicegramSettings.ShareChannelsToggle" = "Condividi le informazioni del canale"; "NicegramSettings.ShareStickersToggle" = "Condividi informazioni sugli sticker"; "NicegramSettings.ShareData.Note" = "Contribuisci all'enciclopedia più completa di canali e gruppi Telegram inviando automaticamente informazioni e link sul canale al nostro database. Non collegiamo il tuo profilo Telegram e il canale e non condividiamo i tuoi dati personali."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "registrare"; +"NicegramCallRecord.StopAlertTitle" = "Ferma la registrazione"; +"NicegramCallRecord.StopAlertDescription" = "Vuoi interrompere la registrazione di questa chiamata?"; +"NicegramCallRecord.StopAlertButtonStop" = "Ferma"; +"NicegramCallRecord.StopAlertButtonCancel" = "Cancella"; +"NicegramCallRecord.SavedMessage" = "Chiamata registrata salvata in **Messaggi Salvati**."; + +/*Feed*/ +"NicegramFeed.Title" = "Feed"; +"NicegramFeed.Add" = "Aggiungi al feed"; +"NicegramFeed.Remove" = "Rimuovi dal feed"; diff --git a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings index f93eb79f46a..c584adc6b24 100644 --- a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "핀 표시"; "ChatFilter.Admin" = "관리자"; "NiceFeatures.Notifications.Fix" = "원치 않는 알림 비활성화"; -"NiceFeatures.Notifications.FixNotice" = "음소거 된 채팅에서 알림을 받는 경우 유용합니다.\n알림, 계정 라벨 및 미디어 미리보기에서는 답장을 보낼 수 없습니다."; "NiceFeatures.Filters.Header" = "필터 (탭)"; -"NiceFeatures.Filters.Notice" = "사용자 정의 탭 수를 선택하세요.\n필터를 변경하려면 탭을 길게 누르세요."; "NiceFeatures.Filters.ShowBadge" = "배지 표시 (필터)"; "NiceFeatures.UseClassicInfoUi" = "기본 채팅 정보 UI 사용"; @@ -60,7 +58,6 @@ "Folder.Create" = "폴더 생성"; "Folder.Create.Name" = "폴더 이름"; "Folder.Create.Placeholder" = "폴더..."; -"Folder.LimitExceeded" = "죄송합니다. 사용자 지정 폴더는 3 개까지만 만들 수 있습니다.\n프리미엄 에서 더 많은 폴더를 사용할 수 있습니다."; "NiceFeatures.HideNumber" = "설정에서 전화번호 숨기기"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Nicegram 지원은 유일한 개발자 및 커뮤니티에 의해 수행됩니다.\n\n먼저 Nicegram FAQ 를 살펴보세요. 대부분의 질문에 대한 중요한 문제 해결 팁과 답변이 있습니다."; "IAP.Premium.Title" = "프리미엄"; "IAP.Premium.Subtitle" = "지나칠 수 없는 독특한 기능!"; -"IAP.Premium.Features" = "빠른 메세지 번역기\n\n종료시 선택한 폴더 기억하기"; "IAP.Premium.Activated" = "프리미엄 활성화!"; "IAP.Common.Restore" = "구매 복원"; "IAP.Common.CantPay" = "죄송합니다. 기기 또는 계정 제한으로 인해 구매할 수 없습니다."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "빠른 번역 버튼"; "Premium.IgnoreTranslate.Title" = "제외한 언어"; "Premium.IgnoreTranslate.Header" = "아래에서 선택한 언어는 빠른 번역 버튼이 비활성화 됩니다."; +"Premium.RecordAllCalls" = "모든 통화 녹음"; /*Manage Filters*/ "ManageFilters.Title" = "탭 관리"; @@ -112,7 +108,9 @@ "Messages.TranslateError" = "죄송합니다. 번역 할 수 없습니다."; "Messages.SpeechToText" = "음성-텍스트 변환"; "Messages.UndoSpeechToText" = "음성-텍스트 변환 취소"; -"Messages.SelectAllFromUser" = "이 사용자에게서 온 모든 메시지 선택 \n\n"; +"Messages.SelectAllFromUser" = "이 사용자에게서 온 모든 메시지 선택 + +"; "Messages.ToLanguage" = "언어"; "Messages.ToLanguage.WithCode" = "언어: %@"; "Messages.TranslateError.ToLanguageNotFound" = "텍스트를 번역할 언어를 결정할 수 없습니다. 수동으로 선택하고 다시 시도하십시오."; @@ -134,7 +132,6 @@ "NiceFeatures.BackupSettings.Done" = "저장된 메시지로 백업 전송"; "NiceFeatures.BackupSettings.Error" = "백업 생성 중 오류가 발생했습니다."; -"NiceFeatures.RestoreSettings.Confirm" = "파일에서 폴더 및 설정을 복원 하시겠습니까?\n⚠️ 현재 데이터를 덮어 씌웁니다"; "NiceFeatures.RestoreSettings.Done" = "폴더 및 설정이 성공적으로 복원되었습니다"; "NiceFeatures.RestoreSettings.Error" = "설정을 복원하는 중에 오류가 발생했습니다. 파일이 손상되었을 수 있습니다"; @@ -145,8 +142,8 @@ "Gmod" = "미리보기 모드"; "Gmod.Enable" = "미리보기 모드를 사용 하시겠습니까?"; "Gmod.Disable" = "미리보기를 사용 중지 하시겠습니까?"; -"Gmod.Notice" = "텔레그램 개인 정보 설정에 의해 온라인 상태가 모든 사람에게 표시되지 않습니다.\n비공개 채팅에 들어가면 앱에 경고가 표시됩니다.\n메시지를 보내거나 입력하면 온라인 상태가 공개 될 수 있습니다."; +"ShowNicegramButtonInChat" = "채팅에서 Nicegram 버튼 보이기"; "SendWithKb" = "«입력»버튼으로 보내기"; "NiceFeatures.ShowGmodIcon" = "미리보기 모드 아이콘 표시"; "Gmod.OpenChatQ" = "대화방 열기"; @@ -188,21 +185,6 @@ "DoubleBottom.Enabled.OK" = "확인"; "DoubleBottom.Passcode.Error" = "비밀번호 잠금에 사용하는 비밀번호와 다른 비밀번호를 설정하세요."; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "다음"; -"NicegramOnboarding.1.Title" = "텔레그램 메신저용 №1 클라이언트"; -"NicegramOnboarding.1.Desc" = "2백만 명이 넘는 Nicegram 사용자와 함께 가장 강력하고 안전한 사무용 텔레그램 클라이언트를 이용해보세요."; -"NicegramOnboarding.2.Title" = "향상된 메시지 전송을 경험"; -"NicegramOnboarding.2.Desc" = "내장 번역기, 음성을 텍스트로 변환, 발신자 표시 없이 메시지 전달, 즐겨찾기에 빠르게 저장 등 여러 특별한 기능으로 대화 속도를 높여 보세요."; -"NicegramOnboarding.3.Title" = "확장된 사용자 프로필"; -"NicegramOnboarding.3.Desc" = "완전 무료로 필요한 만큼 텔레그램 계정을 생성하고, 아이디와 등록 날짜 및 클릭 링크가 표시된 고급 사용자 프로필을 확인할 수 있습니다."; -"NicegramOnboarding.4.Title" = "특별한 사업 확장"; -"NicegramOnboarding.4.Desc" = "Nicegram 팀의 혁신적인 기능을 활용하고, 133개 국가에서 인터넷 사용이 가능한 eSIM을 구매하세요. eSIM 데이터 요금제 전화번호에 새로운 텔레그램 계정을 등록할 수 있습니다."; -"NicegramOnboarding.5.Title" = "가장 안전한 메신저"; -"NicegramOnboarding.5.Desc" = "강력한 암호화, 단체 오디오 및 비디오 통화, 공용 채널, 그룹 및 봇, 채팅과 미디어 및 문서 공유를 위한 무제한 클라우드 스토리지 기능을 갖춘 안전한 메신저입니다."; -"NicegramOnboarding.6.Title" = "릴리를 만나보세요"; -"NicegramOnboarding.6.Desc" = "생활을 간소화하는 개인 AI 챗봇 비서!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "둥근 비디오로 보내기"; "RoundedVideos.MoreButtonTooltip" = "한 번의 탭으로 정사각형 비디오를 원형으로 변환하여 보내기."; @@ -222,6 +204,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "봇 정보 공유하기"; -"NicegramSettings.ShareChannelsToggle" = "채널 정보 공유하기"; +"NicegramSettings.ShareChannelsToggle" = "채널 정보 공유"; "NicegramSettings.ShareStickersToggle" = "스티커 정보 공유하기"; "NicegramSettings.ShareData.Note" = "자동으로 채널 정보와 링크를 제출함으로써 Telegram 채널과 그룹들의 가장 포괄적인 위키백과에 기여하세요. 우리는 여러분의 Telegram 프로필과 채널을 연결하거나 개인 데이터를 공유하지 않습니다."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "녹음"; +"NicegramCallRecord.StopAlertTitle" = "녹음 중지"; +"NicegramCallRecord.StopAlertDescription" = "이 통화 녹음을 중지하시겠습니까?"; +"NicegramCallRecord.StopAlertButtonStop" = "중지"; +"NicegramCallRecord.StopAlertButtonCancel" = "Cancel"; +"NicegramCallRecord.SavedMessage" = "녹음된 통화가 **저장된 메시지**에 저장되었습니다."; + +/*Feed*/ +"NicegramFeed.Title" = "피드"; +"NicegramFeed.Add" = "피드에 추가"; +"NicegramFeed.Remove" = "피드에서 제거"; diff --git a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings index da62bd6b7bf..7b5b70dafe5 100644 --- a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Pokaż przypiętą"; "ChatFilter.Admin" = "Administrator"; "NiceFeatures.Notifications.Fix" = "Wyłącz niechciane powiadomienia"; -"NiceFeatures.Notifications.FixNotice" = "Przydatne, jeśli otrzymujesz powiadomienia z czatów z wyłączonymi powiadomieniami.\nOpcja „Odpowiedz” nie będzie dostępna dla powiadomień, etykiet kont i podglądów multimediów."; "NiceFeatures.Filters.Header" = "FILTRY (ZAKŁADKI)"; -"NiceFeatures.Filters.Notice" = "Wybierz liczbę własnych kart.\nPrzytrzymaj kartę, aby zmienić filtr."; "NiceFeatures.Filters.ShowBadge" = "Pokaż plakietki (filtry)"; "NiceFeatures.UseClassicInfoUi" = "Użyj klasycznego wyglądu Informacji o czacie"; @@ -60,7 +58,6 @@ "Folder.Create" = "Utwórz folder..."; "Folder.Create.Name" = "Nazwa folderu"; "Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Przepraszamy, nie możesz utworzyć więcej niż 3 niestandardowe foldery.\nWięcej folderów jest dostępnych w Premium."; "NiceFeatures.HideNumber" = "Ukryj numer telefonu w ustawieniach"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Wsparcie Nicegram jest wykonywane przez jednego programistę i społeczność.\n\nPo pierwsze, zajrzyj do FAQ: zawiera ważne wskazówki dotyczące rozwiązywania problemów i odpowiedzi na większość pytań."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Unikalne funkcje, których nie można odmówić!"; -"IAP.Premium.Features" = "Szybkie tłumaczenie wiadomości\n\nZapamiętaj wybrany folder przy wyjściu"; "IAP.Premium.Activated" = "Aktywowano wersję Premium!"; "IAP.Common.Restore" = "Odtwórz zakupione"; "IAP.Common.CantPay" = "Nie możesz dokonywać zakupów z powodu ograniczeń dotyczących twojego urządzenia lub konta."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Przycisk Szybkie tłumaczenie"; "Premium.IgnoreTranslate.Title" = "Ignorowane języki"; "Premium.IgnoreTranslate.Header" = "Przycisk „Szybkie tłumaczenie” zostanie WYŁĄCZONY dla języków, które wybierzesz poniżej."; +"Premium.RecordAllCalls" = "Nagrywaj wszystkie rozmowy"; /*Manage Filters*/ "ManageFilters.Title" = "Zarządzaj zakładkami"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Kopia zapasowa została wysłana do Zapisanych wiadomości"; "NiceFeatures.BackupSettings.Error" = "Błąd tworzenia kopii zapasowej."; -"NiceFeatures.RestoreSettings.Confirm" = "Czy na pewno chcesz przywrócić foldery i ustawienia z pliku?\n⚠ Zastąpi to bieżące dane"; "NiceFeatures.RestoreSettings.Done" = "Foldery i ustawienia zostały pomyślnie przywrócone"; "NiceFeatures.RestoreSettings.Error" = "Błąd podczas przywracania ustawień. Plik może być uszkodzony"; @@ -145,8 +140,8 @@ "Gmod" = "Tryb Podglądu"; "Gmod.Enable" = "Włączyć tryb Podglądu?"; "Gmod.Disable" = "Wyłączyć tryb Podglądu?"; -"Gmod.Notice" = "Twój status online zostanie ukryty przed wszystkimi w Ustawieniach prywatności Telegrama.\nAplikacja wyświetli ostrzeżenie, jeśli wejdziesz na prywatny czat.\nTwój status online może zostać ujawniony, jeśli wyślesz lub napiszesz JAKĄŚ wiadomość."; +"ShowNicegramButtonInChat" = "Pokaż przycisk Nicegram w czacie"; "SendWithKb" = "Wyślij za pomocą przycisku «Enter»"; "NiceFeatures.ShowGmodIcon" = "Pokaż ikonę trybu Podglądu"; "Gmod.OpenChatQ" = "Otworzyć czat?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Proszę ustaw inny kod dostępu, różny od tego, którego używasz do blokady kodem dostępu"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Kontynuuj"; -"NicegramOnboarding.1.Title" = "Klient nr 1 dla wiadomości Telegrama"; -"NicegramOnboarding.1.Desc" = "Dołącz do ponad 2 milionów użytkowników Nicegram i uzyskaj dostęp do najpotężniejszego i najbezpieczniejszego klienta Telegram dla biznesu."; -"NicegramOnboarding.2.Title" = "Zaawansowane wrażenia z przesyłania wiadomości"; -"NicegramOnboarding.2.Desc" = "Przyspiesz komunikację dzięki unikalnym funkcjom, takim jak wbudowany tłumacz, mowa na tekst, wysyłanie wiadomości bez podawania autora i szybkie zapisywanie do ulubionych."; -"NicegramOnboarding.3.Title" = "Rozszerzony profil użytkownika"; -"NicegramOnboarding.3.Desc" = "Utwórz tyle kont Telegram, ile potrzebujesz – całkowicie bezpłatnie, wyświetl też zaawansowany profil użytkownika z ID, datą rejestracji i klikalnymi linkami."; -"NicegramOnboarding.4.Title" = "Wyjątkowe elementy biznesowe"; -"NicegramOnboarding.4.Desc" = "Skorzystaj z innowacji zespołu Nicegram i zdobądź eSIM z dostępem do Internetu w 133 krajach. Możesz również zarejestrować nowe konta Telegram na numer telefonu planu danych eSIM."; -"NicegramOnboarding.5.Title" = "Najbezpieczniejszy komunikator"; -"NicegramOnboarding.5.Desc" = "Bezpieczny komunikator z silnym szyfrowaniem, grupowym rozmowami audio i wideo, publicznymi kanałami, grupami i botami, nieograniczonym przechowywaniem w chmurze, udostępnianiem czatów, multimediów i dokumentów."; -"NicegramOnboarding.6.Title" = "Spotkaj Lily"; -"NicegramOnboarding.6.Desc" = "Twój osobisty asystent chatbot SI do upraszczania życia!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Wyślij jako zaokrąglone wideo"; "RoundedVideos.MoreButtonTooltip" = "Przekształć kwadratowe wideo w koła jednym dotknięciem."; @@ -225,3 +205,16 @@ "NicegramSettings.ShareChannelsToggle" = "Udostępnij informacje o kanale"; "NicegramSettings.ShareStickersToggle" = "Udostępnij informacje o naklejkach"; "NicegramSettings.ShareData.Note" = "Przyczyn się do najbardziej kompleksowej wikipedii kanałów i grup Telegrama, automatycznie przesyłając informacje o kanale i link do naszej bazy danych. Nie łączymy Twojego profilu na Telegramie z kanałem i nie udostępniamy Twoich danych osobowych."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "nagraj"; +"NicegramCallRecord.StopAlertTitle" = "Zatrzymaj nagrywanie"; +"NicegramCallRecord.StopAlertDescription" = "Czy chcesz zatrzymać nagrywanie tego połączenia?"; +"NicegramCallRecord.StopAlertButtonStop" = "Zatrzymaj"; +"NicegramCallRecord.StopAlertButtonCancel" = "Anuluj"; +"NicegramCallRecord.SavedMessage" = "Zapisane rozmowy zapisano w **Zapisane Wiadomości**."; + +/*Feed*/ +"NicegramFeed.Title" = "Kanał"; +"NicegramFeed.Add" = "Dodaj do kanału"; +"NicegramFeed.Remove" = "Usuń z kanału"; diff --git a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings index 7ea9e485fef..e1e7839470c 100644 --- a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Mostrar Marcador"; "ChatFilter.Admin" = "Administrador"; "NiceFeatures.Notifications.Fix" = "Desativar Notificações Indesejadas"; -"NiceFeatures.Notifications.FixNotice" = "Útil se você receber notificações de chats silenciados.\nA resposta das notificações, rótulos da conta e visualizações da mídia não estarão disponíveis."; "NiceFeatures.Filters.Header" = "FILTROS (ABAS)"; -"NiceFeatures.Filters.Notice" = "Selecione o número de abas personalizadas.\nToque longo na guia para alterar o filtro."; "NiceFeatures.Filters.ShowBadge" = "Mostrar Badges (Filtros)"; "NiceFeatures.UseClassicInfoUi" = "Use UI clássico nas informações do chat"; @@ -60,7 +58,6 @@ "Folder.Create" = "Criar pasta..."; "Folder.Create.Name" = "Nome da Pasta"; "Folder.Create.Placeholder" = "Pasta..."; -"Folder.LimitExceeded" = "Desculpe, você não pode criar mais do que 3 pastas personalizadas.\nMais pastas estão disponíveis no Premium."; "NiceFeatures.HideNumber" = "Ocultar telefone nas configurações"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Nicegram FAQ"; -"Common.FAQ.Intro" = "Por favor, note que o suporte do Nicegram é feito pelo desenvolvedor e comunidade.\n\nPrimeiramente, dê uma olhada no Nicegram FAQ: ele tem dicas e respostas importantes para a maioria das perguntas."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Recursos exclusivos que você não pode recusar!"; -"IAP.Premium.Features" = "Lembre-se da pasta selecionada ao sair"; "IAP.Premium.Activated" = "Premium ativado!"; "IAP.Common.Restore" = "Restaurar Compras"; "IAP.Common.CantPay" = "Desculpe, mas você não pode fazer compras devido a restrições no seu dispositivo ou conta."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Botão de Tradução Rápida"; "Premium.IgnoreTranslate.Title" = "Idiomas Ignorados"; "Premium.IgnoreTranslate.Header" = "O botão De traduzir esta DESATIVADO para os idiomas que você escolher abaixo."; +"Premium.RecordAllCalls" = "Gravar todas as chamadas"; /*Manage Filters*/ "ManageFilters.Title" = "Administrar Abas"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Backup enviado para Mensagens Salvas"; "NiceFeatures.BackupSettings.Error" = "Erro ao criar backup"; -"NiceFeatures.RestoreSettings.Confirm" = "Tem certeza de que deseja restaurar pastas e configurações do arquivo?\n⚠️ Ele irá substituir os dados atuais"; "NiceFeatures.RestoreSettings.Done" = "Pastas e Configurações restauradas com sucesso"; "NiceFeatures.RestoreSettings.Error" = "Erro ao restaurar as configurações. O arquivo pode estar corrompido"; @@ -145,8 +140,8 @@ "Gmod" = "Modo Pré-visualização"; "Gmod.Enable" = "Ativar modo pré-visualização?"; "Gmod.Disable" = "Desativar modo de pré-visualização?"; -"Gmod.Notice" = "Seu status online será ocultado para todos pelas configurações de privacidade do Telegram.\nO aplicativo irá mostrar um aviso se você estiver entrando em um chat privado.\nSeu status online pode ser revelado se você enviar ou digitar QUALQUER mensagem."; +"ShowNicegramButtonInChat" = "Mostrar botão Nicegram no chat"; "SendWithKb" = "Enviar usando o botão «Enter»"; "NiceFeatures.ShowGmodIcon" = "Mostrar ícone do modo pré-visualização"; "Gmod.OpenChatQ" = "Abrir Chat?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Por favor, defina outra senha, diferente da que você usa como senha de bloqueio"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Continuar"; -"NicegramOnboarding.1.Title" = "O Cliente Nº 1 para o Telegram Messenger"; -"NicegramOnboarding.1.Desc" = "Junte-se aos mais de 2 milhões de usuários do Nicegram e tenha acesso ao mais potente e seguro cliente do Telegram para negócios."; -"NicegramOnboarding.2.Title" = "Experiência Avançada de Sistema de Mensagens"; -"NicegramOnboarding.2.Desc" = "Acelere suas comunicações com recursos únicos como o Tradutor Integrado, Conversão de Fala em Texto, Encaminhamento de mensagens sem especificar o autor e Salvamento rápido nos favoritos. "; -"NicegramOnboarding.3.Title" = "Perfil Estendido de Usuário"; -"NicegramOnboarding.3.Desc" = "Crie quantas contas no Telegram você precisar gratuitamente e tenha acesso a um perfil avançado de usuário com ID, data de cadastro e links clicáveis. "; -"NicegramOnboarding.4.Title" = "Extensões Únicas para Negócios"; -"NicegramOnboarding.4.Desc" = "Aproveite as inovações da equipe do Nicegram e receba um eSIM com acesso à internet em 133 países. Você também pode cadastrar novas contas no Telegram utilizando o número de telefone do plano de dados do eSIM."; -"NicegramOnboarding.5.Title" = "O Aplicativo de Mensagens Mais Seguro"; -"NicegramOnboarding.5.Desc" = "Proteja o Aplicativo de Mensagens com Criptografia de Alta Segurança, Chamadas de Áudio e Vídeo em Grupo, Canais Públicos, Grupos e Bots, Armazenamento Ilimitado em Nuvem para as Conversas, compartilhamento de Mídia e Documentos."; -"NicegramOnboarding.6.Title" = "Conheça a Lily"; -"NicegramOnboarding.6.Desc" = "Seu assistente pessoal de IA Chatbot para simplificar sua vida!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Enviar como Vídeo Arredondado"; "RoundedVideos.MoreButtonTooltip" = "Converta vídeos quadrados em círculos para enviar com apenas um toque."; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Partilhar informação de bots"; -"NicegramSettings.ShareChannelsToggle" = "Partilhar informação de canais"; +"NicegramSettings.ShareChannelsToggle" = "Compartilhar informações do canal"; "NicegramSettings.ShareStickersToggle" = "Partilhar informação de stickers"; "NicegramSettings.ShareData.Note" = "Contribua para a wikipedia mais abrangente de canais e grupos do Telegram, submetendo automaticamente informações e links de canais à nossa base de dados. Não associamos o seu perfil do Telegram ao canal e não partilhamos os seus dados pessoais."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "gravar"; +"NicegramCallRecord.StopAlertTitle" = "Parar gravação"; +"NicegramCallRecord.StopAlertDescription" = "Você deseja parar a gravação desta chamada?"; +"NicegramCallRecord.StopAlertButtonStop" = "Parar"; +"NicegramCallRecord.StopAlertButtonCancel" = "Cancelar"; +"NicegramCallRecord.SavedMessage" = "Chamada gravada salva em **Mensagens Salvas**."; + +/*Feed*/ +"NicegramFeed.Title" = "Feed"; +"NicegramFeed.Add" = "Adicionar ao feed"; +"NicegramFeed.Remove" = "Remover do feed"; diff --git a/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings index 07d2e4b3b8b..63926cce4eb 100644 --- a/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Afișare Pin"; "ChatFilter.Admin" = "Administratori"; "NiceFeatures.Notifications.Fix" = "Dezactivare notificări standard"; -"NiceFeatures.Notifications.FixNotice" = "Este util dacă primiți notificări de la chat-urile dezactivate.\nRăspunsul de la notificări, etichetele contului și previzualizările media nu vor fi disponibile."; "NiceFeatures.Filters.Header" = "FILTRE (FERESTRE)"; -"NiceFeatures.Filters.Notice" = "Selectați numărul de file personalizate.\nApăsați lung pe filă pentru a schimba filtrul."; "NiceFeatures.Filters.ShowBadge" = "Afișați Insigne (Filtre)"; "NiceFeatures.UseClassicInfoUi" = "Utilizează interfața clasică de informații chat"; @@ -60,7 +58,6 @@ "Folder.Create" = "Creaţi folderul..."; "Folder.Create.Name" = "Nume folder"; "Folder.Create.Placeholder" = "Folder..."; -"Folder.LimitExceeded" = "Ne pare rău, nu puteți crea mai mult de 3 foldere personalizate.\nMai multe foldere sunt disponibile achiziţionând varianta Premium."; "NiceFeatures.HideNumber" = "Ascunde telefonul din setări"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "Nicegram Întrebări frecvente"; -"Common.FAQ.Intro" = "Vă rugăm să rețineți că asistența Nicegram este realizată de un singur dezvoltator și de comunitate."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Funcții unice pe care nu le poți refuza!"; -"IAP.Premium.Features" = "Traducător de mesaje rapide\n\nMemorează dosarul selectat la ieșire"; "IAP.Premium.Activated" = "Premium Activat!"; "IAP.Common.Restore" = "Restabiliţi cumpărăturile"; "IAP.Common.CantPay" = "Ne pare rău, dar nu puteți face achiziții din cauza restricțiilor de pe dispozitiv sau cont."; @@ -134,22 +129,78 @@ "NiceFeatures.BackupSettings.Done" = "Copie de rezervă facută pentru Mesajele Salvate"; "NiceFeatures.BackupSettings.Error" = "Eroare la crearea copiei de rezervă"; -"NiceFeatures.RestoreSettings.Confirm" = "Sigur doriți să restaurați folderele și setările din fișier?\n⚠️ Va anula datele actuale"; "NiceFeatures.RestoreSettings.Done" = "Dosare & Setarile restaurat cu succes"; "NiceFeatures.RestoreSettings.Error" = "Eroare restabilire setări. Fișierle pot fi corupte"; +/*Preview Mode*/ +"Gmod.Restricted" = "Nu poți trimite mesaje în Modul de Previzualizare."; +"Gmod.Unavailable" = "Modul de Previzualizare Indisponibil"; +"Gmod.Disable.Notice" = "Vizibilitatea ultimei tale vizite va fi setată la «%1»"; +"Gmod" = "Modul de Previzualizare"; +"Gmod.Enable" = "Activați Modul de Previzualizare?"; +"Gmod.Disable" = "Dezactivați Modul de Previzualizare?"; + +"ShowNicegramButtonInChat" = "Afișați butonul Nicegram în Chat"; +"SendWithKb" = "Trimiteți cu butonul «Enter»"; +"NiceFeatures.ShowGmodIcon" = "Afișați iconița Modului de Previzualizare"; +"Gmod.OpenChatQ" = "Deschideți chatul?"; +"Gmod.OpenChatNotice" = "Chatul va fi marcat ca \"citit\", dar puteți previzualiza chatul cu o apăsare lungă (force-touch)"; +"Gmod.OpenChatBtn" = "Da, deschide chatul"; +"Gmod.DisableBtn" = "Dezactivează avertismentele"; + +"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Numărul tău va fi ascuns doar în interfața utilizatorului. Pentru a-l ascunde de alții, te rog folosește setările de confidențialitate."; +"NicegramSettings.Other.showProfileId" = "Afișează ID-ul Profilului"; +"NicegramSettings.Other.showRegDate" = "Afișează Data Înregistrării"; +"NicegramSettings.Unblock.Header" = "Deblochează"; +"NicegramSettings.Unblock.Button" = "Ghid de Deblocare"; +"NicegramSettings.Other.hideReactions" = "Ascunde Reacțiile"; +"NicegramSettings.RoundVideos.DownloadVideos" = "Descarcă videoclipurile în Galerie"; +"Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignoră Limbile"; +"Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Aplicația detectează limba fiecărui mesaj pe dispozitivul tău. Este o sarcină de înaltă performanță care poate afecta durata de viață a bateriei."; +"Premium.rememberFolderOnExit" = "Ține minte folderul curent la ieșire"; +"NicegramSettings.HideStories" = "Ascunde Poveștile"; + /*Registration Date*/ "NiceFeatures.RegDate.OlderThan" = "Mai vechi de"; "NiceFeatures.RegDate.Approximately" = "Aproximativ"; "NiceFeatures.RegDate.NewerThan" = "Mai noi de"; -"DoubleBottom.Enabled.OK" = "OK"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Continuă"; -"NicegramOnboarding.1.Title" = "Client No1 pentru Telegram Messenger"; -"NicegramOnboarding.1.Desc" = "Alătură-te celor peste 2 milioane de utilizatori Nicegram şi obţine acces la cel mai puternic şi sigur client Telegramă pentru afaceri."; -"NicegramOnboarding.2.Title" = "Experiență de mesagerie avansată"; -"NicegramOnboarding.3.Title" = "Profil utilizator extins"; -"NicegramOnboarding.4.Title" = "Extensii unice Business"; -"NicegramOnboarding.5.Title" = "Cel mai sigur Messenger"; -"NicegramOnboarding.5.Desc" = "Securizează Messenger cu o criptare puternică, grup de apeluri audio și video, canale publice, grupuri și bot, stocare nelimitată în Cloud pentru partajarea de chaturi, media și documente."; +/*Quick Replies*/ +"NiceFeatures.QuickReplies" = "Răspunsuri Rapide"; +"NiceFeatures.QuickReplies.Description" = "Puteți adăuga un număr nelimitat de răspunsuri folosind butonul de mai jos. Pentru a folosi răspunsul rapid, introduceți simbolul \"&\" în câmpul de introducere a mesajului."; +"NiceFeatures.QuickReplies.AddNew" = "Adaugă Nou"; +"NiceFeatures.QuickReplies.Placeholder" = "Introduceți Textul"; + +/*Telegram Premium*/ +"TelegramPremium.Description" = "Din păcate, nu puteți obține Funcțiile Premium Telegram în timp ce sunteți autentificat în aplicația Nicegram. Vă rugăm să mergeți la aplicația oficială Telegram și să vă abonați la Telegram Premium. După ce reveniți la Nicegram, toate Funcțiile Premium vor fi disponibile pentru utilizare."; + +/*Double Bottom*/ +"DoubleBottom.Title" = "Double bottom"; +"DoubleBottom.Description" = "Pentru a activa această funcție, ar trebui să aveți mai mult de un cont și să activați Blocarea prin Cod de Acces (mergeți la Setări → Confidențialitate și Securitate → Blocare prin Cod de Acces)"; +"DoubleBottom.Enabled.Title" = "Dublu Fund este activat"; +"DoubleBottom.Enabled.Description" = "Vă rugăm să vă amintiți codul de acces pe care tocmai l-ați setat și să reporniți aplicația pentru ca Dublu Fund să funcționeze bine"; +"DoubleBottom.Enabled.OK" = "OK"; +"DoubleBottom.Passcode.Error" = "Vă rugăm să setați un alt cod de acces diferit de cel pe care îl utilizați pentru Blocarea prin Cod de Acces"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Trimite ca Video Rotund"; +"RoundedVideos.MoreButtonTooltip" = "Convertește videoclipurile pătrate pentru a le trimite ca cercuri cu un singur atingere."; +"RoundedVideos.SendButtonTooltip" = "Apăsați lung pentru a trimite videoclipul dvs. ca un cerc stilat."; + +/*Speech to text*/ +"SpeechToText.UseOpenAi" = "OpenAI Speech2Text"; +"SpeechToText.Toast" = "[Abonați-vă la Nicegram Premium]() pentru tehnologii OpenAI Speech2Text mai rapide și mai bune!"; + +/*Confirm Call*/ +"ConfirmCall.Desc" = "Sunteți sigur că doriți să efectuați un apel?"; + +/*Hidden Chats*/ +"ChatContextMenu.Hide" = "Ascunde"; +"ChatContextMenu.Unhide" = "Dezvăluie"; +"HiddenChatsTooltip" = "Apăsați și mențineți apăsat logo-ul 'N' pentru a dezvălui conversațiile secrete ascunse"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Distribuie informații despre boți"; +"NicegramSettings.ShareStickersToggle" = "Distribuie informații despre stickere"; +"NicegramSettings.ShareData.Note" = "Contribuie la cea mai cuprinzătoare wikipedia a canalelor și grupurilor Telegram prin trimiterea automată a informațiilor și a linkului canalului în baza noastră de date. Nu conectăm profilul dvs. de Telegram și canalul și nu împărtășim datele dvs. personale."; +"NicegramCallRecord.StopAlertButtonCancel" = "Anulează"; diff --git a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings index aa1a46a937b..0accae50d52 100644 --- a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Показать Пин"; "ChatFilter.Admin" = "Админ"; "NiceFeatures.Notifications.Fix" = "Отключить нежелательные уведомления"; -"NiceFeatures.Notifications.FixNotice" = "Нужно, если вы получаете уведомления, даже если они отключены в чате. Ответ из уведомления, метки аккаунтов и предпросмотр медиа будут недоступны."; "NiceFeatures.Filters.Header" = "ФИЛЬТРЫ (ВКЛАДКИ)"; -"NiceFeatures.Filters.Notice" = "Выберите количество пользовательских вкладок.\nДолгое нажатие по вкладке позволит сменить фильтр."; "NiceFeatures.Filters.ShowBadge" = "Показывать бейджи (Фильтры)"; "NiceFeatures.UseClassicInfoUi" = "Классический интерфейс «о чате»"; @@ -60,7 +58,6 @@ "Folder.Create" = "Создать Папку..."; "Folder.Create.Name" = "Название Папки"; "Folder.Create.Placeholder" = "Папка..."; -"Folder.LimitExceeded" = "К сожалению, создать можно не больше 3 папок. Совсем скоро можно будет создать больше."; "NiceFeatures.HideNumber" = "Скрыть номер в настройках"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_ru"; "Common.FAQUrl" = "https://nicegram.app/ru/faq/"; "Common.FAQ.Button" = "Вопросы о Nicegram"; -"Common.FAQ.Intro" = "Пожалуйста, обратите внимание, что поддержка Nicegram осуществляется одним единственным разработчиком и сообществом.\n\nВ первую очередь, ознакомьтесь с часто задаваемыми вопросами о Nicegram: там Вы найдёте ответы на большинство вопросов и важные советы по устранению неполадок."; "IAP.Premium.Title" = "Премиум"; "IAP.Premium.Subtitle" = "Уникальные функции, от которых трудно отказаться!"; -"IAP.Premium.Features" = "Быстрая кнопка перевода\n\nЗапоминание папки при выходе"; "IAP.Premium.Activated" = "Премиум Активирован!"; "IAP.Common.Restore" = "Восстановить Покупки"; "IAP.Common.CantPay" = "К сожалению, Вы не можете производить покупки ввиду ограничений вашего устройства или аккаунта."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Кнопка быстрого перевода"; "Premium.IgnoreTranslate.Title" = "Игнорируемые языки"; "Premium.IgnoreTranslate.Header" = "Кнопка быстрого перевода будет ОТКЛЮЧЕНА для выбранных ниже языков."; +"Premium.RecordAllCalls" = "Записывать все звонки"; /*Manage Filters*/ "ManageFilters.Title" = "Управление вкладками"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "Резервное копирование выполнено в сохраненные сообщения"; "NiceFeatures.BackupSettings.Error" = "Ошибка при создании бэкапа"; -"NiceFeatures.RestoreSettings.Confirm" = "Восстановить Папки и Настройки из файла?\n⚠️ Текущие данные будут перезаписаны"; "NiceFeatures.RestoreSettings.Done" = "Папки и настройки успешно восстановлены"; "NiceFeatures.RestoreSettings.Error" = "Ошибка при восстановлении настроек. Возможно, файл повреждён"; @@ -145,8 +140,8 @@ "Gmod" = "Режим предпросмотра"; "Gmod.Enable" = "Включить режим предпросмотра?"; "Gmod.Disable" = "Отключить режим предпросмотра?"; -"Gmod.Notice" = "Ваш онлайн статус будет скрыт от всех в настройках конфиденциальности Telegram.\nПриложение предупредит, если вы захотите открыть личный чат.\nВаш онлайн статус может быть виден, если вы отправите или напечатаете ЛЮБОЕ сообщение."; +"ShowNicegramButtonInChat" = "Показать кнопку Nicegram в чате"; "SendWithKb" = "Отправка кнопкой «Enter»"; "NiceFeatures.ShowGmodIcon" = "Иконка режима предпросмотра"; "Gmod.OpenChatQ" = "Открыть чат?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "Ок"; "DoubleBottom.Passcode.Error" = "Установите другой код доступа, отличающийся от того, который Вы используете для Блокировки кодом доступа."; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Продолжить"; -"NicegramOnboarding.1.Title" = "Клиент №1 для мессенджера Telegram"; -"NicegramOnboarding.1.Desc" = "Присоединяйтесь к более чем 2 миллионам пользователей Nicegram и получите доступ к самому мощному и безопасному клиенту Telegram для бизнеса."; -"NicegramOnboarding.2.Title" = "Улучшенный обмен сообщениями"; -"NicegramOnboarding.2.Desc" = "Ускорьте общение благодаря таким уникальным функциям, как встроенный переводчик, перевод речи в текст, пересылка сообщений без указания автора и быстрое сохранение в избранное."; -"NicegramOnboarding.3.Title" = "Расширенные профили пользователей"; -"NicegramOnboarding.3.Desc" = "Создавайте сколько угодно аккаунтов Telegram совершенно бесплатно, а также просматривайте расширенные профили пользователей с идентификаторами, датами регистрации и интерактивными ссылками."; -"NicegramOnboarding.4.Title" = "Уникальные бизнес-расширения"; -"NicegramOnboarding.4.Desc" = "Воспользуйтесь инновациями от команды Nicegram и получите eSIM с доступом в интернет в 133 странах. Вы также можете зарегистрировать новые аккаунты Telegram на номер телефона тарифного плана eSIM."; -"NicegramOnboarding.5.Title" = "Самый безопасный мессенджер"; -"NicegramOnboarding.5.Desc" = "Безопасный мессенджер с надежным шифрованием, групповыми аудио- и видеозвонками, общедоступными каналами, группами и ботами, неограниченным облачным хранилищем для чатов, а также возможностью обмена медиафайлами и документами."; -"NicegramOnboarding.6.Title" = "Встречайте ИИ-чатбота Лили!"; -"NicegramOnboarding.6.Desc" = "Ваш личный ИИ-помощник, призванный облегчить вам жизнь!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Отправить как круглое видео"; "RoundedVideos.MoreButtonTooltip" = "Конвертируйте квадратные видео в круглые одним нажатием."; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Поделиться информацией о ботах"; -"NicegramSettings.ShareChannelsToggle" = "Поделиться информацией о канале"; +"NicegramSettings.ShareChannelsToggle" = "Поделиться информацией о каналах"; "NicegramSettings.ShareStickersToggle" = "Поделиться информацией о стикерах"; "NicegramSettings.ShareData.Note" = "Внесите свой вклад в самую полную википедию каналов и групп Telegram, автоматически отправляя информацию о канале и ссылку в нашу базу данных. Мы не связываем ваш профиль Telegram и канал и не делимся вашими личными данными."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "записать"; +"NicegramCallRecord.StopAlertTitle" = "Остановить запись"; +"NicegramCallRecord.StopAlertDescription" = "Вы хотите остановить запись этого звонка?"; +"NicegramCallRecord.StopAlertButtonStop" = "Стоп"; +"NicegramCallRecord.StopAlertButtonCancel" = "Закрыть"; +"NicegramCallRecord.SavedMessage" = "Записанный звонок сохранен в **Избранное**."; + +/*Feed*/ +"NicegramFeed.Title" = "Лента"; +"NicegramFeed.Add" = "Добавить в ленту"; +"NicegramFeed.Remove" = "Удалить из ленты"; diff --git a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings index f8d40272807..e87433122ec 100644 --- a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "Sabiti Göster"; "ChatFilter.Admin" = "Yönetici"; "NiceFeatures.Notifications.Fix" = "İstenmeyen Bildirimleri Devre Dışı Bırak"; -"NiceFeatures.Notifications.FixNotice" = "Sessiz sohbetlerden bildirimler alıyorsanız kullanışlıdır.\nBildirimlerden yanıtlama, hesap etiketleri ve medya önizlemeleri kullanılamaz olacak."; "NiceFeatures.Filters.Header" = "FİLTRELER (SEKMELER)"; -"NiceFeatures.Filters.Notice" = "Özel sekmelerin sayısını seçin.\nFiltreyi değiştirmek için sekmeye uzun basın."; "NiceFeatures.Filters.ShowBadge" = "Rozetleri Göster (Filtreler)"; "NiceFeatures.UseClassicInfoUi" = "Klasik Sohbet Bilgisi Arayüzünü Kullan"; @@ -60,7 +58,6 @@ "Folder.Create" = "Klasör Oluştur..."; "Folder.Create.Name" = "Klasör İsmi"; "Folder.Create.Placeholder" = "Klasör..."; -"Folder.LimitExceeded" = "Üzgünüz, 3’ten fazla özel klasör oluşturamazsınız.\nDaha fazla klasör Premium’da kullanılabilir."; "NiceFeatures.HideNumber" = "Ayarlarda numarayı gizle"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_tr"; "Common.FAQUrl" = "https://nicegram.app/tr/faq/"; "Common.FAQ.Button" = "Nicegram SSS"; -"Common.FAQ.Intro" = "Lütfen Nicegram Destek’in sadece geliştirici ve topluluktan ibaret olduğunu unutmayın.\n\nİlk olarak, Nicegram SSS’e göz atın: önemli çözüm ipuçları ve birçok soruya cevaplar var."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Reddedemeyeceğiniz benzersiz özellikler!"; -"IAP.Premium.Features" = "Hızlı Mesaj Çevirmeni\n\nÇıkışta seçili klasörü hatırla"; "IAP.Premium.Activated" = "Premium Aktive Edildi!"; "IAP.Common.Restore" = "Satın Alımları Geri Yükle"; "IAP.Common.CantPay" = "Üzgünüz, ama cihaz veya hesap kısıtlamalarınız yüzünden satın alım yapamazsınız."; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "Hızlı Çeviri butonu"; "Premium.IgnoreTranslate.Title" = "İstenmeyen Diller"; "Premium.IgnoreTranslate.Header" = "Hızlı Çeviri butonu, aşağıda seçtiğiniz diller için DEVRE DIŞI bırakılacaktır."; +"Premium.RecordAllCalls" = "Tüm çağrıları kaydet"; /*Manage Filters*/ "ManageFilters.Title" = "Sekmeleri Yönet"; @@ -115,7 +111,8 @@ "Messages.SelectAllFromUser" = "Bu Kullanıcıdan Olan Bütün Mesajları Seç"; "Messages.ToLanguage" = "Hedef Dil"; "Messages.ToLanguage.WithCode" = "Hedef Dil: %@"; -"Messages.TranslateError.ToLanguageNotFound" = "Metni çevirmek istediğiniz dili tespit edemedik. Lütfen manuel olarak seçin ve tekrar deneyin.\n"; +"Messages.TranslateError.ToLanguageNotFound" = "Metni çevirmek istediğiniz dili tespit edemedik. Lütfen manuel olarak seçin ve tekrar deneyin. +"; "Messages.ReplyPrivately" = "Özelden Cevapla"; "Messages.DeleteAllSystemMessages" = "Tümünü Sil"; @@ -134,7 +131,6 @@ "NiceFeatures.BackupSettings.Done" = "Yedekleme, Kayıtlı Mesajlar’a gönderildi"; "NiceFeatures.BackupSettings.Error" = "Yedekleme oluşturulurken hata"; -"NiceFeatures.RestoreSettings.Confirm" = "Klasör & Ayarları dosyadan geri yüklemek istediğinize emin misiniz?\n⚠️ Şu ankinin üzerine yazılacak"; "NiceFeatures.RestoreSettings.Done" = "Klasör & Ayarlar başarılı bir şekilde geri yüklendi"; "NiceFeatures.RestoreSettings.Error" = "Ayarlar geri yüklenirken hata. Dosya bozulmuş olabilir"; @@ -145,8 +141,8 @@ "Gmod" = "Önizleme Modu"; "Gmod.Enable" = "Önizleme Modu etkinleştirilsin mi?"; "Gmod.Disable" = "Önizleme Modu devre dışı bırakılsın mı?"; -"Gmod.Notice" = "Çevrimiçi durumunuz, Telegram gizlilik ayarlarına göre herkesten gizlenecek.\nUygulama, siz özel bir sohbete girerken bir uyarı gösterecek.\nHERHANGİ BİR mesaj yazar veya gönderirseniz, çevrimiçi durumunuz görünebilir."; +"ShowNicegramButtonInChat" = "Sohbette Nicegram Butonunu Göster"; "SendWithKb" = "«Enter» butonu ile gönder"; "NiceFeatures.ShowGmodIcon" = "Önizleme Modu simgesini göster"; "Gmod.OpenChatQ" = "Sohbet açılsın mı?"; @@ -188,21 +184,6 @@ "DoubleBottom.Enabled.OK" = "TAMAM"; "DoubleBottom.Passcode.Error" = "Lütfen Parola Kilidi için mevcut kullandığınızdan farklı bir parola belirleyin"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "Devam et"; -"NicegramOnboarding.1.Title" = "Telegram Messenger için 1 Numaralı İstemci"; -"NicegramOnboarding.1.Desc" = "2 Milyondan fazla Nicegram kullanıcısına katılın ve iş için en güçlü ve güvenli Telegram istemcisine erişin. "; -"NicegramOnboarding.2.Title" = "Gelişmiş Mesajlaşma Tecrübesi"; -"NicegramOnboarding.2.Desc" = "Dahili Çevirmen, Konuşmayı Metne Çevirme, Konuşmayı yazar belirtmeden iletme ve Sık kullanılanlara hızlı kaydetme gibi özgün özellikleri ile iletişiminizi hızlandırın. "; -"NicegramOnboarding.3.Title" = "Genişletilmiş Kullanıcı Profili"; -"NicegramOnboarding.3.Desc" = "Tamamen ücretsiz bir şekilde istediğiniz kadar Telegram hesabı oluşturun ve bununla birlikte kimlik, kayıt tarihi ve tıklanabilir bağlantılar ile gelişmiş bir kullanıcı profili görüntüleyin."; -"NicegramOnboarding.4.Title" = "Özgün İşletme Uzantıları"; -"NicegramOnboarding.4.Desc" = "Nicegram takımının yeniliklerinden faydalanın ve 133 ülkede internet erişimi olan bir eSIM'e sahip olun. Aynı zamanda eSIM veri planının telefon numarasına yeni Telegram hesapları da kaydedebilirsiniz. "; -"NicegramOnboarding.5.Title" = "En Güvenli Mesenger"; -"NicegramOnboarding.5.Desc" = "Güçlü Şifreleme, Grup Sesli ve Görüntülü Aramalar, Genel kanallar, Gruplar ve Botlar, Sohbetler Belgeler ve Dökümanlar için Limitsiz Bulut depolaması ile Güvenli Messenger."; -"NicegramOnboarding.6.Title" = "Lily ile Tanışın"; -"NicegramOnboarding.6.Desc" = "Hayatınızı Basitleştirmek için Kişisel Yapay Zeka Sohbet Robotu Asistanınız!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "Yuvarlak Video Olarak Gönder"; "RoundedVideos.MoreButtonTooltip" = "Kare videoları tek dokunuşla daire olarak göndermek için dönüştürün."; @@ -222,6 +203,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "Bot bilgilerini paylaş"; -"NicegramSettings.ShareChannelsToggle" = "Kanal bilgilerini paylaş"; +"NicegramSettings.ShareChannelsToggle" = "Kanal bilgisini paylaş"; "NicegramSettings.ShareStickersToggle" = "Sticker bilgilerini paylaş"; "NicegramSettings.ShareData.Note" = "Kanal bilgileri ve bağlantısını otomatik olarak göndererek Telegram kanalları ve gruplarının en kapsamlı vikipedisine katkıda bulunun. Telegram profilinizle ve kanalınızla bağlantı kurmuyoruz ve kişisel verilerinizi paylaşmıyoruz."; + +/*Call Record*/ +"NicegramCallRecord.Title" = "kaydet"; +"NicegramCallRecord.StopAlertTitle" = "Kaydı durdur"; +"NicegramCallRecord.StopAlertDescription" = "Bu çağrıyı kaydetmeyi durdurmak istiyor musunuz?"; +"NicegramCallRecord.StopAlertButtonStop" = "Durdur"; +"NicegramCallRecord.StopAlertButtonCancel" = "İptal"; +"NicegramCallRecord.SavedMessage" = "Kaydedilen Çağrı **Kaydedilen Mesajlar** bölümüne kaydedildi."; + +/*Feed*/ +"NicegramFeed.Title" = "Akış"; +"NicegramFeed.Add" = "Akışa ekle"; +"NicegramFeed.Remove" = "Akıştan kaldır"; diff --git a/Telegram/Telegram-iOS/uz.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/uz.lproj/NiceLocalizable.strings new file mode 100644 index 00000000000..d9007156242 --- /dev/null +++ b/Telegram/Telegram-iOS/uz.lproj/NiceLocalizable.strings @@ -0,0 +1,206 @@ +"AppName" = "Nicegram"; + +/*Common*/ +"Nicegram.PrivacyPolicy" = "Maxfiylik siyosati"; +"Nicegram.EULA" = "EULA"; + +/*ChatFilter*/ +"ChatFilter.Bots" = "Bot"; +"ChatFilter.Channels" = "Kanal"; +"ChatFilter.Private" = "Foydalanuvchi"; +"ChatFilter.Groups" = "Guruh"; +"ChatFilter.Unread" = "O'qilmagan"; +"ChatFilter.Unmuted" = "Ovozsiz"; +"ChatFilter.Favourites" = "Tanlanganlar"; + +/*Nice Features*/ +"NiceFeatures.Title" = "Nicegram"; +"NiceFeatures.Tabs.Header" = "Yorliq"; +"NiceFeatures.Tabs.ShowContacts" = "Kontaktlar yorlig'ini ko'rsatish"; +"NiceFeatures.ChatScreen.Header" = "CHAT EKRANI"; +"NiceFeatures.ChatScreen.AnimatedStickers" = "Animatsion stiker"; +"NiceFeatures.useBackCam" = "Orqa kameradan foydalaning (dumaloq video)"; + +"NiceFeatures.Folders.Header" = "Jildlar"; +"NiceFeatures.Account.Header" = "HISOB SOZLAMALARI"; +"NiceFeatures.Folders.TgFolders" = "Pastki qismida papkalar"; +"NiceFeatures.Folders.TgFolders.Notice" = "iOS uslubidagi papkalar ro'yxati"; + +"NiceFeatures.RoundVideos.Header" = "DUMALOQ VIDEOLAR"; +"NiceFeatures.RoundVideos.UseRearCamera" = "Orqa kameradan boshlang"; + +"NiceFeatures.InstantLock" = "Darhol"; + +/*Save to Cloud*/ +"Chat.SaveToCloud" = "Bulutga saqlash"; + +/*Open Pin*/ +"Chat.OpenPin" = "Pinni ko'rsatish"; +"ChatFilter.Admin" = "Admin"; +"NiceFeatures.Notifications.Fix" = "Keraksiz bildirishnomalarni o'chirish"; +"NiceFeatures.Filters.Header" = "FILTRLAR (YORLIQLAR)"; +"NiceFeatures.Filters.ShowBadge" = "Belgilarni ko‘rsatish (filtrlar)"; +"NiceFeatures.UseClassicInfoUi" = "Klassik Chat Info UI dan foydalaning"; + +/*Common*/ +"Common.ExitNow" = "Hozir chiqish"; +"Common.Later" = "Keyinchalik"; +"Common.RestartRequired" = "Qayta ishga tushirish kerak!"; +"NiceFeatures.Tabs.ShowNames" = "Varaq nomlarini ko'rsatish"; +"Chat.ForwardAsCopy" = "Nusxa sifatida yo‘naltirish"; + +/*Folder*/ +"Folder.DefaultName" = "Jild"; +"Folder.New" = "Yangi Jild"; +"Folder.Created" = "Jild yaratildi"; +"Folder.AddToExisting" = "Mavjud jildga qo'shish"; +"Folder.Updated" = "Jild yangilandi"; +"Folder.Create" = "Jild yaratish..."; +"Folder.Create.Name" = "Jild nomi"; +"Folder.Create.Placeholder" = "Jild..."; +"NiceFeatures.HideNumber" = "Sozlamalarda telefon raqamni yashirish"; + +/*NGWeb*/ +"NGWeb.Blocked" = "AppStore yoʻriqnomalari tufayli Nicegram’da mavjud emas"; + +/*Browser*/ +/*Please, Go to «Data & Storage» to configure default browser*/ +"NiceFeatures.Use.DataStorage" = "Iltimos, standart brauzerni sozlash uchun «%1» dan foydalaning"; +"NiceFeatures.Browser.Header" = "URLs"; +"NiceFeatures.Browser.UseBrowser" = "Brauzerda havolalarni oching"; +"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram havolalarni ilova ichida emas, balki tashqi brauzerda ochadi. Tanlangan brauzer o'rnatilishi kerak."; +"ChatFilter.Missed" = "O'tkazib yuborilgan"; + +/*Premium*/ +"Premium.Title" = "Premium"; +"Premium.UnlimitedPins.Header" = "CHEKSIZ QO'SHILGAN CHATLAR"; +"Premium.SyncPins" = "Mahkamlangan chatlarni sinxronlash"; +"Premium.SyncPins.Notice.ON" = "Agar siz boshqa mijozda mahkamlangan chatlarni o'zgartirsangiz, ular Nicegramda O'ZGARADI."; +"Premium.SyncPins.Notice.OFF" = "Agar siz boshqa mijozda mahkamlangan chatlarni o'zgartirsangiz, ular Nicegramda O'ZGARMAYDI."; +"Premium.Missed.Header" = "O'tkazib yuborilgan xabarlar"; +"Premium.Missed" = "O'tkazib yuborilgan xabarlar haqida xabar bering"; +"Premium.Missed.Notice" = "Uzoq kechikishdan so'ng (uyqu, o'qish va h.k.) Ilovani ochsangiz, Nicegram sizga o'qilmagan shaxsiy xabarlar va eslatmalar haqida xabar beradi."; +"Folder.DeleteAsk" = "Jildni o'chirish"; +"Folder.NeedPremium" = "Bu jild faqat Premium bilan mavjud. Siz Premiumga ega bo'lishingiz yoki surish orqali jildni o'chirishingiz mumkin."; +"Common.SupportChatUsername" = "nicegramchat"; +"Common.FAQUrl" = "https://nicegram.app/faq/"; +"Common.FAQ.Button" = "Nicegram FAQ"; +"IAP.Premium.Title" = "Premium"; +"IAP.Premium.Subtitle" = "Siz rad eta olmaydigan noyob xususiyatlar!"; +"IAP.Premium.Activated" = "Premium faollashtirilgan!"; +"IAP.Common.Restore" = "Xaridlarni tiklash"; +"IAP.Common.CantPay" = "Kechirasiz, qurilmangiz yoki hisobingizdagi cheklovlar tufayli xarid qila olmaysiz."; +"IAP.Common.ErrorFetch" = "Kechirasiz, App Store xaridlarini olib bo'lmadi."; +"IAP.Common.Congrats" = "Tabriklaymiz!"; +"IAP.Common.ValidateError" = "Kechirasiz, xaridingizni tasdiqlab bo‘lmadi."; +"IAP.Common.Connecting" = "AppStore-ga ulanmoqda..."; +"Premium.OnetapTranslate" = "Tez tarjima tugmasi"; +"Premium.IgnoreTranslate.Title" = "E'tiborga olinmagan tillar"; +"Premium.IgnoreTranslate.Header" = "Siz quyida tanlagan tillar uchun Tezkor Tarjima tugmasi OʻCHIRILDI."; + +/*Manage Filters*/ +"ManageFilters.Title" = "Yorliqlarni boshqarish"; +"ManageFilters.Header" = "MAVJUD FILTRLARNI SOZLASH"; + +"Messages.Translate" = "Tarjima"; +"Messages.UndoTranslate" = "Tarjimani bekor qilish"; +"Messages.TranslateError" = "Kechirasiz, tarjima mavjud emas."; +"Messages.SpeechToText" = "Nutq2Matn"; +"Messages.UndoSpeechToText" = "Nutq2Matnni bekor qilish"; +"Messages.SelectAllFromUser" = "Bu foydalanuvchidan hammasini tanlang"; +"Messages.ToLanguage" = "Tilga"; +"Messages.ToLanguage.WithCode" = "Tilga: %@"; +"Messages.TranslateError.ToLanguageNotFound" = "Matnni tarjima qilmoqchi boʻlgan tilni aniqlay olmadik. Uni qoʻlda tanlang va qaytadan urinib koʻring."; +"Messages.ReplyPrivately" = "Shaxsiy javob bering"; +"Messages.DeleteAllSystemMessages" = "Hammasini o'chirish"; + +/*NG Lab Registration Date*/ +"NGLab.RegDate.Btn" = "Ro'yxatdan o'tish sanasini oling"; +"NGLab.RegDate.Title" = "Roʻyxatdan oʻtgan sana"; +"NGLab.RegDate.Notice" = "Bu taxminiy sana"; +"NGLab.RegDate.MenuItem" = "ro'yxatdan o'tgan"; +"NGLab.RegDate.FetchError" = "Kechirasiz, roʻyxatdan oʻtish sanasi aniqlanmadi."; +"NGLab.BadDeviceToken" = "[iOS 11+] Qurilmani tekshirib bo‘lmadi."; + +/*Backup Settings*/ +"NiceFeatures.BackupIcloud" = "Sozlamalar va papkalarni iCloud bilan sinxronlang"; +"NiceFeatures.BackupSettings" = "Zaxiralash sozlamalari va papkalar"; +"NiceFeatures.BackupSettings.Notice" = "Zaxira faylini yaratadi. Qayta tiklash uchun faylga teging."; +"NiceFeatures.BackupSettings.Done" = "Zaxira nusxasi Saqlangan xabarlarga yuborildi"; +"NiceFeatures.BackupSettings.Error" = "Zaxira nusxasini yaratishda xatolik yuz berdi"; + +"NiceFeatures.RestoreSettings.Done" = "Jildlar va sozlamalar muvaffaqiyatli tiklandi"; +"NiceFeatures.RestoreSettings.Error" = "Sozlamalarni tiklashda xatolik yuz berdi. Fayl buzilgan bo'lishi mumkin"; + +/*Preview Mode*/ +"Gmod.Restricted" = "Ko‘rib chiqish rejimida xabarlarni yubora olmaysiz."; +"Gmod.Unavailable" = "Ko‘rib chiqish rejimi mavjud emas"; +"Gmod.Disable.Notice" = "Oxirgi koʻrish darajasi «%1»ga oʻrnatiladi"; +"Gmod" = "Ko‘rib chiqish rejimi"; +"Gmod.Enable" = "Ko‘rib chiqish rejimi yoqilsinmi?"; +"Gmod.Disable" = "Ko‘rib chiqish rejimi o‘chirilsinmi?"; + +"ShowNicegramButtonInChat" = "Chatda Nicegram tugmachasini ko'rsatish"; +"SendWithKb" = "\"Enter\" tugmasi bilan yuboring"; +"NiceFeatures.ShowGmodIcon" = "Ko'rib chiqish rejimi belgisini ko'rsatish"; +"Gmod.OpenChatQ" = "Chat ochilsinmi?"; +"Gmod.OpenChatNotice" = "Chat “o‘qilgan” deb belgilanadi, lekin siz uzoq bosish (majburiy teginish) orqali suhbatni oldindan ko‘rishingiz mumkin"; +"Gmod.OpenChatBtn" = "Ha, chatni oching"; +"Gmod.DisableBtn" = "Ogohlantirishlarni o'chiring"; + +"NicegramSettings.Other.hidePhoneInSettingsNotice" = "Sizning raqamingiz faqat foydalanuvchi interfeysida yashirin bo'ladi. Uni boshqalardan yashirish uchun Maxfiylik sozlamalaridan foydalaning."; +"NicegramSettings.Other.showProfileId" = "Profil ID ko'rsatish"; +"NicegramSettings.Other.showRegDate" = "Roʻyxatdan oʻtgan sanani koʻrsatish"; +"NicegramSettings.Unblock.Header" = "Blokdan chiqarish"; +"NicegramSettings.Unblock.Button" = "Blokdan chiqarish yoʻriqnomasi"; +"NicegramSettings.Other.hideReactions" = "Reaksiyalarni yashirish"; +"NicegramSettings.RoundVideos.DownloadVideos" = "Galereyaga videolarni yuklab oling"; +"Premium.OnetapTranslate.UseIgnoreLanguages" = "Tillarga e'tibor bermaslik"; +"Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Ilova qurilmangizdagi har bir xabarning tilini aniqlaydi. Bu batareyaning ishlash muddatiga ta'sir qilishi mumkin bo'lgan yuqori samarali vazifa."; +"Premium.rememberFolderOnExit" = "dan Kattaroq"; +"NicegramSettings.HideStories" = "Hikoyalarni yashirish"; + +/*Registration Date*/ +"NiceFeatures.RegDate.OlderThan" = "dan kattaroq"; +"NiceFeatures.RegDate.Approximately" = "Taxminan"; +"NiceFeatures.RegDate.NewerThan" = "dan yangiroq"; + +/*Quick Replies*/ +"NiceFeatures.QuickReplies" = "Tez javoblar"; +"NiceFeatures.QuickReplies.Description" = "Quyidagi tugma yordamida cheksiz miqdordagi javoblarni qo'shishingiz mumkin. Tez javobdan foydalanish uchun xabar kiritish maydoniga ** “&” ** belgisini kiriting."; +"NiceFeatures.QuickReplies.AddNew" = "Yangi qo'shish"; +"NiceFeatures.QuickReplies.Placeholder" = "Matnni kiriting"; + +/*Telegram Premium*/ +"TelegramPremium.Description" = "Afsuski, Nicegram ilovasiga kirganingizda Telegram Premium funksiyalaridan foydalana olmaysiz. Rasmiy Telegram ilovasiga oʻting va Telegram Premiumga obuna boʻling. Nicegramga qaytsangiz, barcha Premium funksiyalardan foydalanishingiz mumkin bo'ladi."; + +/*Double Bottom*/ +"DoubleBottom.Title" = "Ikkita pastki"; +"DoubleBottom.Description" = "Bu funksiyani yoqish uchun sizda bir nechta hisob qaydnomasi boʻlishi va parolni bloklash funksiyasini yoqishingiz kerak (Sozlamalar → Maxfiylik va xavfsizlik → Parolni bloklash boʻlimiga oʻting)"; +"DoubleBottom.Enabled.Title" = "Ikkita pastki yoqilgan"; +"DoubleBottom.Enabled.Description" = "Iltimos, siz o'rnatgan parolni eslab qoling va yaxshi ishlashi uchun ilovani ikki marta pastga tushiring"; +"DoubleBottom.Enabled.OK" = "OK"; +"DoubleBottom.Passcode.Error" = "Iltimos, parolni qulflash uchun ishlatadigan boshqa parolni o'rnating"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Yumaloq video sifatida yuboring"; +"RoundedVideos.MoreButtonTooltip" = "Bir tegish bilan doira shaklida yuborish uchun kvadrat videolarni aylantiring."; +"RoundedVideos.SendButtonTooltip" = "Videongizni zamonaviy doira shaklida yuborish uchun uzoq bosing."; + +/*Speech to text*/ +"SpeechToText.UseOpenAi" = "OpenAI Speech2Text"; +"SpeechToText.Toast" = "Tezroq va yaxshiroq OpenAI Speech2Text texnologiyalari uchun [Nicegram Premium-ga obuna bo'ling]()!"; + +/*Confirm Call*/ +"ConfirmCall.Desc" = "Haqiqatan ham qo'ng'iroq qilmoqchimisiz?"; + +/*Hidden Chats*/ +"ChatContextMenu.Hide" = "Yashirish"; +"ChatContextMenu.Unhide" = "Ko'rsatish"; +"HiddenChatsTooltip" = "Yashirin maxfiy suhbatlarni ochish uchun \"N\" logotipini bosing va ushlab turing"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Botlar haqida ma'lumot ulashing"; +"NicegramSettings.ShareStickersToggle" = "Stiker ma'lumotlarini baham ko'ring"; +"NicegramSettings.ShareData.Note" = "Kanal ma'lumotlarini avtomatik ravishda yuborish va ma'lumotlar bazasiga havola qilish orqali Telegram kanallari va guruhlari haqidagi eng to'liq vikipediyaga hissa qo'shing. Biz sizning telegram profilingiz va kanalingizni bog'lamaymiz va shaxsiy ma'lumotlaringizni baham ko'rmaymiz."; +"NicegramCallRecord.StopAlertButtonCancel" = "Bekor qilish"; diff --git a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings index bff18cd0737..6e95de1d81f 100644 --- a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "显示置顶"; "ChatFilter.Admin" = "管理员"; "NiceFeatures.Notifications.Fix" = "禁用多余通知"; -"NiceFeatures.Notifications.FixNotice" = "如果在已关闭通知群组/频道仍然会收到通知,那开启此功能可以禁用此通知.\n开启此功能,在通知界面回复消息,帐户标签和媒体预览将会被禁用。"; "NiceFeatures.Filters.Header" = "标签过滤"; -"NiceFeatures.Filters.Notice" = "选择自定义标签的数量。\n长按标签更改标签选项。"; "NiceFeatures.Filters.ShowBadge" = "标签上显示角标"; "NiceFeatures.UseClassicInfoUi" = "使用经典对话信息界面"; @@ -60,7 +58,6 @@ "Folder.Create" = "创建分组..."; "Folder.Create.Name" = "分组名"; "Folder.Create.Placeholder" = "分组..."; -"Folder.LimitExceeded" = "抱歉,您最多只能创建3个自定义分组。\n升级至 Premium 可创建更多分组。"; "NiceFeatures.HideNumber" = "设置中隐藏手机号码"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_cn"; "Common.FAQUrl" = "https://nicegram.app/cn/faq/"; "Common.FAQ.Button" = "Nicegram 常见问题"; -"Common.FAQ.Intro" = "请注意:Nicegram 的支持服务由开发者和社区提供。\n\n如用疑问,请前往“Nicegram 常见问题”,那里有常见问题和故障排除指南。"; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "您无法拒绝的独特功能!"; -"IAP.Premium.Features" = "快速消息翻译器\n\n退出时记住选定的文件夹"; "IAP.Premium.Activated" = "Premium 已激活!"; "IAP.Common.Restore" = "恢复购买"; "IAP.Common.CantPay" = "抱歉,由于您的设备或帐户限制,您无法购买。"; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "快速翻译按钮"; "Premium.IgnoreTranslate.Title" = "忽略的语言"; "Premium.IgnoreTranslate.Header" = "对于您在下面选择的语言,快速翻译按钮将被禁用。"; +"Premium.RecordAllCalls" = "录制所有通话"; /*Manage Filters*/ "ManageFilters.Title" = "管理标签"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "备份已完成,存放在\"收藏夹\"里。"; "NiceFeatures.BackupSettings.Error" = "创建备份出错"; -"NiceFeatures.RestoreSettings.Confirm" = "您确定要从备份文件中恢复分组和设置吗?\n⚠️ 这将会覆盖当前的数据"; "NiceFeatures.RestoreSettings.Done" = "分组和设置已恢复成功"; "NiceFeatures.RestoreSettings.Error" = "恢复设置出错,备份文件可能已损坏。"; @@ -145,8 +140,8 @@ "Gmod" = "幽灵模式"; "Gmod.Enable" = "启用幽灵模式?"; "Gmod.Disable" = "关闭幽灵模式?"; -"Gmod.Notice" = "通过 Telegram 隐私设置,您的在线状态会对所有人隐藏。\n如果您进入私有对话,将会显示警告提示。\n如果您在对话中发送或编辑任何消息,您的在线状态可能会显示出来。"; +"ShowNicegramButtonInChat" = "在聊天中显示 Nicegram 按钮"; "SendWithKb" = "按<回车>发送"; "NiceFeatures.ShowGmodIcon" = "显示幽灵模式图标"; "Gmod.OpenChatQ" = "打开对话?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "确认"; "DoubleBottom.Passcode.Error" = "请设置另一个与密码锁不同的密码"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "继续"; -"NicegramOnboarding.1.Title" = "Telegram 通讯的首选客户端"; -"NicegramOnboarding.1.Desc" = "加入 200 多万 Nicegram 用户,使用最强大且安全的 Telegram 客户端开展业务。"; -"NicegramOnboarding.2.Title" = "高级的通讯体验"; -"NicegramOnboarding.2.Desc" = "凭借独特的功能加速您的交流,比如内置翻译器,语音转文字,隐藏作者转发消息,以及快速保存到收藏夹。"; -"NicegramOnboarding.3.Title" = "扩展用户个人资料"; -"NicegramOnboarding.3.Desc" = "根据需要,想创建多少 Telegram 账户都可以,完全免费;还可以查看显示 ID、注册日期和可点击链接的高级用户个人资料。"; -"NicegramOnboarding.4.Title" = "独特的商业扩展能力"; -"NicegramOnboarding.4.Desc" = "利用 Nicegram 团队的创新,获取可在 133 个国家上网的 eSIM。您也可以用 eSIM 数据计划的电话号码注册新的 Telegram 账户。"; -"NicegramOnboarding.5.Title" = "最安全的通讯工具"; -"NicegramOnboarding.5.Desc" = "安全的通讯工具,拥有强加密、群组音频和视频电话、公共频道、群聊和机器人、无限制的聊天云存储空间、媒体和文档共享功能。"; -"NicegramOnboarding.6.Title" = "认识莉莉"; -"NicegramOnboarding.6.Desc" = "您的个人人工智能聊天机器人助手,简化您的生活!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "以圆形视频发送"; "RoundedVideos.MoreButtonTooltip" = "一键将方形视频转换为圆形发送。"; @@ -225,3 +205,16 @@ "NicegramSettings.ShareChannelsToggle" = "分享频道信息"; "NicegramSettings.ShareStickersToggle" = "分享贴纸信息"; "NicegramSettings.ShareData.Note" = "通过自动提交频道信息和链接到我们的数据库,为最全面的Telegram频道和群组维基百科做出贡献。我们不会连接您的Telegram个人资料和频道,也不会分享您的个人数据。"; + +/*Call Record*/ +"NicegramCallRecord.Title" = "记录"; +"NicegramCallRecord.StopAlertTitle" = "停止录音"; +"NicegramCallRecord.StopAlertDescription" = "您想停止录制这个通话吗?"; +"NicegramCallRecord.StopAlertButtonStop" = "停止"; +"NicegramCallRecord.StopAlertButtonCancel" = "取消"; +"NicegramCallRecord.SavedMessage" = "已将录制的通话保存到 **保存的消息** 中。"; + +/*Feed*/ +"NicegramFeed.Title" = "动态"; +"NicegramFeed.Add" = "添加到订阅源"; +"NicegramFeed.Remove" = "从订阅源中移除"; diff --git a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings index 78c1b13372e..3ef4b20b860 100644 --- a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings @@ -38,9 +38,7 @@ "Chat.OpenPin" = "顯示置頂"; "ChatFilter.Admin" = "管理員"; "NiceFeatures.Notifications.Fix" = "停用非必要通知"; -"NiceFeatures.Notifications.FixNotice" = "如果您想將「已關閉通知」的對話完全禁音,這將非常有用。\n來自回覆、@username、媒體預覽的通知將被停用。"; "NiceFeatures.Filters.Header" = "篩選標籤"; -"NiceFeatures.Filters.Notice" = "選擇篩選標籤的數量。\n長按標籤來更改篩選器。"; "NiceFeatures.Filters.ShowBadge" = "顯示標記 (小紅點)"; "NiceFeatures.UseClassicInfoUi" = "使用經典版介面"; @@ -60,7 +58,6 @@ "Folder.Create" = "建立資料夾…"; "Folder.Create.Name" = "資料夾名稱"; "Folder.Create.Placeholder" = "資料夾…"; -"Folder.LimitExceeded" = "抱歉,您最多只能建立 3 個自訂資料夾。\n進階版將可建立更多資料夾。"; "NiceFeatures.HideNumber" = "在設定中隱藏電話號碼"; /*NGWeb*/ @@ -88,10 +85,8 @@ "Common.SupportChatUsername" = "nicegram_tw"; "Common.FAQUrl" = "https://nicegram-tw.gitbook.io/"; "Common.FAQ.Button" = "Nicegram 常見問題"; -"Common.FAQ.Intro" = "注意:Nicegram 支援僅由開發人員和志願者提供。\n\n請先看一下 Nicegram 常見問題:它有故障排除指示和其他大多數問題的解答。"; -"IAP.Premium.Title" = "Premium"; +"IAP.Premium.Title" = "進階版功能"; "IAP.Premium.Subtitle" = "您無法抗拒的獨特功能!"; -"IAP.Premium.Features" = "訊息快速翻譯"; "IAP.Premium.Activated" = "已升級進階版!"; "IAP.Common.Restore" = "恢復購買"; "IAP.Common.CantPay" = "抱歉,由於您的設備或帳戶限制,您無法購買。"; @@ -102,6 +97,7 @@ "Premium.OnetapTranslate" = "快速翻譯按鈕"; "Premium.IgnoreTranslate.Title" = "忽略的語言"; "Premium.IgnoreTranslate.Header" = "對於以下選擇的語言,「快速翻譯」按鈕將被停用。"; +"Premium.RecordAllCalls" = "錄製所有通話"; /*Manage Filters*/ "ManageFilters.Title" = "管理標籤"; @@ -134,7 +130,6 @@ "NiceFeatures.BackupSettings.Done" = "備份已儲存"; "NiceFeatures.BackupSettings.Error" = "建立備份檔時發生錯誤"; -"NiceFeatures.RestoreSettings.Confirm" = "您確定要從備份檔還原「文件夾和設定」?\n⚠️它將覆蓋當前數據"; "NiceFeatures.RestoreSettings.Done" = "已成功還原「文件夾與設定」"; "NiceFeatures.RestoreSettings.Error" = "恢復設定錯誤,備份檔可能已損壞"; @@ -145,8 +140,8 @@ "Gmod" = "預覽模式"; "Gmod.Enable" = "啟用預覽模式?"; "Gmod.Disable" = "停用預覽模式?"; -"Gmod.Notice" = "您的線上狀態會被 Telegram 隱私設定隱藏。\n如果您進入私人對話,App 會顯示警告。\n如果您傳送或輸入任何訊息,你的線上狀態將可能會被顯示。"; +"ShowNicegramButtonInChat" = "在聊天中顯示 Nicegram 按鈕"; "SendWithKb" = "點擊 «Enter» 傳送"; "NiceFeatures.ShowGmodIcon" = "顯示預覽模式圖示"; "Gmod.OpenChatQ" = "開啟對話?"; @@ -188,21 +183,6 @@ "DoubleBottom.Enabled.OK" = "好"; "DoubleBottom.Passcode.Error" = "請設定另一個與你的密碼鎖不同的密碼。"; -/*Onboarding*/ -"NicegramOnboarding.Continue" = "繼續"; -"NicegramOnboarding.1.Title" = "第一名的 Telegram 即時通訊用戶端"; -"NicegramOnboarding.1.Desc" = "加入超過 200 萬名 Nicegram 用戶的行列,並獲得使用最強大與最安全的商用 Telegram 用戶端的機會。"; -"NicegramOnboarding.2.Title" = "進階的即時通訊體驗"; -"NicegramOnboarding.2.Desc" = "使用獨特功能如內建翻譯器、語音轉文本、不需指定作者即可轉傳訊息以及快速儲存到我的最愛等來提升您的溝通速度。"; -"NicegramOnboarding.3.Title" = "擴充的使用者檔案"; -"NicegramOnboarding.3.Desc" = "創建 Telegram 帳戶,要多少設多少且完全免費,並瀏覽具有 ID、註冊日期以及可點擊連結之進階用戶資料。"; -"NicegramOnboarding.4.Title" = "獨特的商用擴充元件"; -"NicegramOnboarding.4.Desc" = "利用 Nicegram 團隊的創新技術並取得可在 133 個國家上網的 eSIM。您還可以用 eSIM 數據方案的電話號碼註冊新的 Telegram 帳戶。"; -"NicegramOnboarding.5.Title" = "最安全的即時通訊程式"; -"NicegramOnboarding.5.Desc" = "具有高強度加密、群組語音及視訊通話、公用頻道、群組和機器人、可用於聊天、媒體和文件共享的無限雲端儲存之安全即時通訊程式。"; -"NicegramOnboarding.6.Title" = "认识莉莉"; -"NicegramOnboarding.6.Desc" = "您的个人人工智能聊天机器人助手,简化您的生活!"; - /*Rounded Videos*/ "RoundedVideos.ButtonTitle" = "以圓形影片發送"; "RoundedVideos.MoreButtonTooltip" = "一鍵將方形影片轉換為圓形發送。"; @@ -222,6 +202,19 @@ /*Data Sharing*/ "NicegramSettings.ShareBotsToggle" = "分享機器人資訊"; -"NicegramSettings.ShareChannelsToggle" = "分享頻道資訊"; +"NicegramSettings.ShareChannelsToggle" = "分享頻道信息"; "NicegramSettings.ShareStickersToggle" = "分享貼圖資訊"; "NicegramSettings.ShareData.Note" = "通過自動提交頻道資訊和連結到我們的數據庫,貢獻給最全面的Telegram頻道和群組維基百科。我們不會連接您的Telegram個人檔案和頻道,也不會分享您的個人資料。"; + +/*Call Record*/ +"NicegramCallRecord.Title" = "記錄"; +"NicegramCallRecord.StopAlertTitle" = "停止錄音"; +"NicegramCallRecord.StopAlertDescription" = "您想停止錄製這個通話嗎?"; +"NicegramCallRecord.StopAlertButtonStop" = "停止"; +"NicegramCallRecord.StopAlertButtonCancel" = "取消"; +"NicegramCallRecord.SavedMessage" = "已將錄製的通話保存到 **保存的消息** 中。"; + +/*Feed*/ +"NicegramFeed.Title" = "動態"; +"NicegramFeed.Add" = "添加到訂閱源"; +"NicegramFeed.Remove" = "從訂閱源中移除"; diff --git a/WORKSPACE.bzlmod b/WORKSPACE.bzlmod index eca8fa2b8cb..956985b5872 100644 --- a/WORKSPACE.bzlmod +++ b/WORKSPACE.bzlmod @@ -18,9 +18,9 @@ http_archive( http_archive( name = "bazel_features", - sha256 = "0f23d75c7623d6dba1fd30513a94860447de87c8824570521fcc966eda3151c2", - strip_prefix = "bazel_features-1.4.1", - url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.4.1/bazel_features-v1.4.1.tar.gz", + sha256 = "bdc12fcbe6076180d835c9dd5b3685d509966191760a0eb10b276025fcb76158", + strip_prefix = "bazel_features-1.17.0", + url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.17.0/bazel_features-v1.17.0.tar.gz", ) http_file( diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 03be8088976..abfbefc3031 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -100,13 +100,13 @@ def __init__(self, bazel, override_bazel_version, override_xcode_version, bazel_ # https://github.com/bazelbuild/rules_swift # Use -Osize instead of -O when building swift modules. - #'--features=swift.opt_uses_osize', + '--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=1', '--swiftcopt=-num-threads', '--swiftcopt=1', - '--swiftcopt=-j1', # Strip unsused code. '--features=dead_strip', @@ -147,18 +147,7 @@ def set_disable_provisioning_profiles(self): self.disable_provisioning_profiles = True def set_configuration(self, configuration): - if configuration == 'debug_universal': - self.configuration_args = [ - # bazel debug build configuration - '-c', 'dbg', - - # Build universal binaries. - '--ios_multi_cpus=armv7,arm64', - - # Always build universal Watch binaries. - '--watchos_cpus=arm64_32' - ] + self.common_debug_args - elif configuration == 'debug_arm64': + if configuration == 'debug_arm64': self.configuration_args = [ # bazel debug build configuration '-c', 'dbg', @@ -188,16 +177,6 @@ def set_configuration(self, configuration): # Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required. '--ios_multi_cpus=sim_arm64', - # Always build universal Watch binaries. - '--watchos_cpus=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=arm64_32' ] + self.common_debug_args @@ -216,41 +195,10 @@ def set_configuration(self, configuration): '--apple_generate_dsym', # Require DSYM files as build output. - '--output_groups=+dsyms' - ] + self.common_release_args - elif configuration == 'release_armv7': - self.configuration_args = [ - # bazel optimized build configuration - '-c', 'opt', + '--output_groups=+dsyms', - # Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required. - '--ios_multi_cpus=armv7', - - # Always build universal Watch binaries. - '--watchos_cpus=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=arm64_32', - - # Generate DSYM files when building. - '--apple_generate_dsym', - - # Require DSYM files as build output. - '--output_groups=+dsyms' + '--swiftcopt=-num-threads', + '--swiftcopt=0', ] + self.common_release_args else: raise Exception('Unknown configuration {}'.format(configuration)) diff --git a/build-system/bazel-rules/apple_support b/build-system/bazel-rules/apple_support index cf271a330b0..07dd08dc404 160000 --- a/build-system/bazel-rules/apple_support +++ b/build-system/bazel-rules/apple_support @@ -1 +1 @@ -Subproject commit cf271a330b08a3bbd8ad61241d03787683d5a1c5 +Subproject commit 07dd08dc40470dcf8c9c9e0f36ca100d99535722 diff --git a/build-system/bazel-rules/rules_apple b/build-system/bazel-rules/rules_apple index 345b71fc226..1fbec0268ca 160000 --- a/build-system/bazel-rules/rules_apple +++ b/build-system/bazel-rules/rules_apple @@ -1 +1 @@ -Subproject commit 345b71fc226d79abfe180b27b7f8d711aa398bbd +Subproject commit 1fbec0268ca5fe31102611ebf543e31a2bad32d1 diff --git a/build-system/bazel-rules/rules_xcodeproj b/build-system/bazel-rules/rules_xcodeproj index db0ce201aa4..44b6f046d95 160000 --- a/build-system/bazel-rules/rules_xcodeproj +++ b/build-system/bazel-rules/rules_xcodeproj @@ -1 +1 @@ -Subproject commit db0ce201aa4f2099559d6e4b4373f7de83b81eff +Subproject commit 44b6f046d95b84933c1149fbf7f9d81fd4e32020 diff --git a/ci/download_translations.sh b/ci/download_translations.sh new file mode 100755 index 00000000000..e2254763640 --- /dev/null +++ b/ci/download_translations.sh @@ -0,0 +1,2 @@ +current_branch=$(git symbolic-ref --short HEAD) +crowdin download --branch $current_branch diff --git a/ci/upload_sources.sh b/ci/upload_sources.sh new file mode 100755 index 00000000000..dbed7e4139e --- /dev/null +++ b/ci/upload_sources.sh @@ -0,0 +1,2 @@ +current_branch=$(git symbolic-ref --short HEAD) +crowdin upload sources --branch $current_branch diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..7570bf2f71a --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,10 @@ +"base_path": "." +"base_url": "https://api.crowdin.com" +"preserve_hierarchy": true +files: [ + { + "source": "/Telegram/**/en.lproj/NiceLocalizable.strings", + "dest": "iOS/nicegram-client/%original_file_name%", + "translation": "/Telegram/**/%two_letters_code%.lproj/%original_file_name%" + } +] diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 31f77b3dec6..93d031ef584 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -462,6 +462,18 @@ public final class NavigateToChatControllerParams { } } + public struct ReportReason { + public let title: String + public let option: Data + public let message: String? + + public init(title: String, option: Data, message: String?) { + self.title = title + self.option = option + self.message = message + } + } + public let navigationController: NavigationController public let chatController: ChatController? public let context: AccountContext @@ -481,7 +493,7 @@ public final class NavigateToChatControllerParams { public let activateMessageSearch: (ChatSearchDomain, String)? public let peekData: ChatPeekTimeout? public let peerNearbyData: ChatPeerNearbyData? - public let reportReason: ReportReason? + public let reportReason: NavigateToChatControllerParams.ReportReason? public let animated: Bool public let options: NavigationAnimationOptions public let parentGroupId: PeerGroupId? @@ -495,7 +507,7 @@ public final class NavigateToChatControllerParams { public let forceOpenChat: Bool public let customChatNavigationStack: [EnginePeer.Id]? - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, forceOpenChat: Bool = false, customChatNavigationStack: [EnginePeer.Id]? = nil) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: NavigateToChatControllerParams.ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, forceOpenChat: Bool = false, customChatNavigationStack: [EnginePeer.Id]? = nil) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -568,6 +580,7 @@ public enum PeerInfoControllerMode { case forumTopic(thread: ChatReplyThreadMessage) case recommendedChannels case myProfile + case myProfileGifts } public enum ContactListActionItemInlineIconPosition { @@ -628,6 +641,7 @@ public enum ChatListSearchFilter: Equatable { case voice case peer(PeerId, Bool, String, String) case date(Int32?, Int32, String) + case publicPosts public var id: Int64 { switch self { @@ -651,6 +665,8 @@ public enum ChatListSearchFilter: Equatable { return 8 case .voice: return 9 + case .publicPosts: + return 10 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): @@ -801,7 +817,7 @@ public enum CollectibleItemInfoScreenSubject { public enum StorySearchControllerScope { - case query(String) + case query(EnginePeer?, String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) } @@ -931,7 +947,7 @@ public protocol SharedAccountContext: AnyObject { func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? - func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController + func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController @@ -964,7 +980,7 @@ public protocol SharedAccountContext: AnyObject { func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal - func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) + func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) @@ -981,7 +997,10 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController + + func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController + func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController @@ -1023,11 +1042,15 @@ public protocol SharedAccountContext: AnyObject { func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController + func makeStarsIntroScreen(context: AccountContext) -> ViewController + func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController + + func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController - func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) func makeDebugSettingsController(context: AccountContext?) -> ViewController? @@ -1141,9 +1164,13 @@ public protocol AccountContext: AnyObject { func chatLocationUnreadCount(for location: ChatLocation, contextHolder: Atomic) -> Signal func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic, messageIndex: MessageIndex) - func scheduleGroupCall(peerId: PeerId) + func scheduleGroupCall(peerId: PeerId, parentController: ViewController) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) +// MARK: Nicegram NCG-6373 Feed tab + var updateFeed: Signal { get } + func needUpdateFeed() +// } public struct AntiSpamBotConfiguration { diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index bc00816ad63..20e920d271d 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -864,10 +864,10 @@ public struct ChatInputQueryCommandsResult: Equatable { public enum ChatPresentationInputQueryResult: Equatable { // MARK: Nicegram QuickReplies - case quickReplies([String]) + case quickReplies([String], String) // case stickers([FoundStickerItem]) - case hashtags([String]) + case hashtags([String], String) case mentions([EnginePeer]) case commands(ChatInputQueryCommandsResult) case emojis([(String, TelegramMediaFile?, String)], NSRange) @@ -876,9 +876,9 @@ public enum ChatPresentationInputQueryResult: Equatable { public static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool { switch lhs { // MARK: Nicegram QuickReplies - case let .quickReplies(lhsResults): - if case let .quickReplies(rhsResults) = rhs { - return lhsResults == rhsResults + case let .quickReplies(lhsResults, lhsQuery): + if case let .quickReplies(rhsResults, rhsQuery) = rhs { + return lhsResults == rhsResults && lhsQuery == rhsQuery } else { return false } @@ -889,9 +889,9 @@ public enum ChatPresentationInputQueryResult: Equatable { } else { return false } - case let .hashtags(lhsResults): - if case let .hashtags(rhsResults) = rhs { - return lhsResults == rhsResults + case let .hashtags(lhsResults, lhsQuery): + if case let .hashtags(rhsResults, rhsQuery) = rhs { + return lhsResults == rhsResults && lhsQuery == rhsQuery } else { return false } @@ -1036,14 +1036,18 @@ public protocol ChatController: ViewController { var visibleContextController: ViewController? { get } + var contentContainerNode: ASDisplayNode { get } + var searching: ValuePromise { get } + var searchResultsCount: ValuePromise { get } + var externalSearchResultsCount: Int32? { get set } var alwaysShowSearchResultsAsList: Bool { get set } var includeSavedPeersInSearchResults: Bool { get set } var showListEmptyResults: Bool { get set } + func beginMessageSearch(_ query: String) func updatePresentationMode(_ mode: ChatControllerPresentationMode) - func beginMessageSearch(_ query: String) func displayPromoAnnouncement(text: String) func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index c16d2956056..243145a8b73 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -101,8 +101,10 @@ public final class ContactSelectionControllerParams { public let multipleSelection: Bool public let requirePhoneNumbers: Bool public let confirmation: (ContactListPeer) -> Signal + public let openProfile: ((EnginePeer) -> Void)? + public let sendMessage: ((EnginePeer) -> Void)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { self.context = context self.updatedPresentationData = updatedPresentationData self.mode = mode @@ -114,5 +116,7 @@ public final class ContactSelectionControllerParams { self.multipleSelection = multipleSelection self.requirePhoneNumbers = requirePhoneNumbers self.confirmation = confirmation + self.openProfile = openProfile + self.sendMessage = sendMessage } } diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 1d9b659b8b9..856bc3530b8 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -21,8 +21,8 @@ public func freeMediaFileResourceInteractiveFetched(account: Account, userLocati return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource)) } -public func freeMediaFileResourceInteractiveFetched(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource) -> Signal { - return fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource)) +public func freeMediaFileResourceInteractiveFetched(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource, range: (Range, MediaBoxFetchPriority)? = nil) -> Signal { + return fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource), range: range) } public func cancelFreeMediaFileInteractiveFetch(account: Account, file: TelegramMediaFile) { diff --git a/submodules/AccountContext/Sources/IsMediaStreamable.swift b/submodules/AccountContext/Sources/IsMediaStreamable.swift index 650911e79ae..34e9a2a0292 100644 --- a/submodules/AccountContext/Sources/IsMediaStreamable.swift +++ b/submodules/AccountContext/Sources/IsMediaStreamable.swift @@ -18,8 +18,8 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _, _) = attribute { - if flags.contains(.supportsStreaming) { + if case let .Video(_, _, flags, _, _, _) = attribute { + if flags.contains(.supportsStreaming) || !media.alternativeRepresentations.isEmpty { return true } break @@ -41,7 +41,7 @@ public func isMediaStreamable(media: TelegramMediaFile) -> Bool { return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.supportsStreaming) { return true } diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 092fc4c52ba..a0e4ecc34aa 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -202,6 +202,7 @@ public protocol UniversalVideoManager: AnyObject { func removePlaybackCompleted(id: AnyHashable, index: Int) func statusSignal(content: UniversalVideoContent) -> Signal func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(RangeSet, Int64)?, NoError> + func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal } public enum AudioRecordingState: Equatable { diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 5cd4fd732e3..9134f6d26b1 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -130,6 +130,7 @@ public enum StarsPurchasePurpose: Equatable { case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) case gift(peerId: EnginePeer.Id) case unlockMedia(requiredStars: Int64) + case starGift(peerId: EnginePeer.Id, requiredStars: Int64) } public struct PremiumConfiguration { @@ -142,6 +143,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, starsGiftsPurchaseAvailable: false, + starGiftsPurchaseBlocked: true, boostsPerGiftCount: 3, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, @@ -169,6 +171,7 @@ public struct PremiumConfiguration { public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool public let starsGiftsPurchaseAvailable: Bool + public let starGiftsPurchaseBlocked: Bool public let boostsPerGiftCount: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 @@ -195,6 +198,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, starsGiftsPurchaseAvailable: Bool, + starGiftsPurchaseBlocked: Bool, boostsPerGiftCount: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, @@ -220,6 +224,7 @@ public struct PremiumConfiguration { self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable + self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked self.boostsPerGiftCount = boostsPerGiftCount self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount @@ -253,6 +258,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, + starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, @@ -276,3 +282,7 @@ public struct PremiumConfiguration { } } } + +public protocol GiftOptionsScreenProtocol { + +} diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index d3e44953ea1..268f4f89e74 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import TelegramCore import SwiftSignalKit import TelegramAudio +import Display +import Postbox public enum RequestCallResult { case requested @@ -413,6 +415,7 @@ public protocol PresentationGroupCall: AnyObject { var members: Signal { get } var audioLevels: Signal<[(EnginePeer.Id, UInt32, Float, Bool)], NoError> { get } var myAudioLevel: Signal { get } + var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { get } var isMuted: Signal { get } var isNoiseSuppressionEnabled: Signal { get } @@ -471,5 +474,12 @@ public protocol PresentationCallManager: AnyObject { func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult - func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult + func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult +// MARK: Nicegram NCG-5828 call recording + var callCompletion: (() -> Void)? { get set } + func startRecordCall(with completion: @escaping () -> Void) + func stopRecordCall() + func setupPeer(peer: EnginePeer) + func showRecordSaveToast() +// } diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index c9333755ff6..80273b5756f 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -10,12 +10,18 @@ import UniversalMediaPlayer import AVFoundation import RangeSet +public enum UniversalVideoContentVideoQuality: Equatable { + case auto + case quality(Int) +} + public protocol UniversalVideoContentNode: AnyObject { var ready: Signal { get } var status: Signal { get } var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { get } + var isNativePictureInPictureActive: Signal { get } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) func play() func pause() @@ -29,11 +35,17 @@ public protocol UniversalVideoContentNode: AnyObject { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) func setBaseRate(_ baseRate: Double) + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) func fetchControl(_ control: UniversalVideoNodeFetchControl) func notifyPlaybackControlsHidden(_ hidden: Bool) func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) + func enterNativePictureInPicture() -> Bool + func exitNativePictureInPicture() + func setNativePictureInPictureIsActive(_ value: Bool) } public protocol UniversalVideoContent { @@ -41,7 +53,7 @@ public protocol UniversalVideoContent { var dimensions: CGSize { get } var duration: Double { get } - func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode + func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode func isEqual(to other: UniversalVideoContent) -> Bool } @@ -61,7 +73,7 @@ public protocol UniversalVideoDecoration: AnyObject { func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) func updateContentNodeSnapshot(_ snapshot: UIView?) - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) func tap() } @@ -83,6 +95,7 @@ public enum UniversalVideoNodeFetchControl { } public final class UniversalVideoNode: ASDisplayNode { + private let accountId: AccountRecordId private let postbox: Postbox private let audioSession: ManagedAudioSession private let manager: UniversalVideoManager @@ -92,7 +105,7 @@ public final class UniversalVideoNode: ASDisplayNode { private let autoplay: Bool private let snapshotContentWhenGone: Bool - private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? + private(set) var contentNode: (UniversalVideoContentNode & ASDisplayNode)? private var contentNodeId: Int32? private var playbackCompletedIndex: Int? @@ -117,6 +130,11 @@ public final class UniversalVideoNode: ASDisplayNode { return self._bufferingStatus.get() } + private let _isNativePictureInPictureActive = Promise() + public var isNativePictureInPictureActive: Signal { + return self._isNativePictureInPictureActive.get() + } + private let _ready = Promise() public var ready: Signal { return self._ready.get() @@ -128,11 +146,12 @@ public final class UniversalVideoNode: ASDisplayNode { if self.canAttachContent { assert(self.contentRequestIndex == nil) + let accountId = self.accountId let content = self.content let postbox = self.postbox let audioSession = self.audioSession self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: { - return content.makeContentNode(postbox: postbox, audioSession: audioSession) + return content.makeContentNode(accountId: accountId, postbox: postbox, audioSession: audioSession) }, update: { [weak self] contentNodeAndFlags in if let strongSelf = self { strongSelf.updateContentNode(contentNodeAndFlags) @@ -153,7 +172,8 @@ public final class UniversalVideoNode: ASDisplayNode { return self.contentNode != nil } - public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + self.accountId = accountId self.postbox = postbox self.audioSession = audioSession self.manager = manager @@ -171,6 +191,7 @@ public final class UniversalVideoNode: ASDisplayNode { self._status.set(self.manager.statusSignal(content: self.content)) self._bufferingStatus.set(self.manager.bufferingStatusSignal(content: self.content)) + self._isNativePictureInPictureActive.set(self.manager.isNativePictureInPictureActiveSignal(content: self.content)) self.decoration.setStatus(self.status) @@ -237,8 +258,8 @@ public final class UniversalVideoNode: ASDisplayNode { } } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.decoration.updateLayout(size: size, transition: transition) + public func updateLayout(size: CGSize, actualSize: CGSize? = nil, transition: ContainedViewLayoutTransition) { + self.decoration.updateLayout(size: size, actualSize: actualSize ?? size, transition: transition) } public func play() { @@ -329,6 +350,34 @@ public final class UniversalVideoNode: ASDisplayNode { }) } + public func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setVideoQuality(videoQuality) + } + }) + } + + public func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + var result: (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode { + result = contentNode.videoQualityState() + } + }) + return result + } + + public func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + var result: Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError>? + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode { + result = contentNode.videoQualityStateSignal() + } + }) + return result ?? .single(nil) + } + public func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { @@ -390,4 +439,30 @@ public final class UniversalVideoNode: ASDisplayNode { } }) } + + public func enterNativePictureInPicture() -> Bool { + var result = false + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + result = contentNode.enterNativePictureInPicture() + } + }) + return result + } + + public func exitNativePictureInPicture() { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.exitNativePictureInPicture() + } + }) + } + + public func setNativePictureInPictureIsActive(_ value: Bool) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setNativePictureInPictureIsActive(value) + } + }) + } } diff --git a/submodules/AlertUI/Sources/ThemedTextAlertController.swift b/submodules/AlertUI/Sources/ThemedTextAlertController.swift index c7ef5ddff0e..d2d40d04648 100644 --- a/submodules/AlertUI/Sources/ThemedTextAlertController.swift +++ b/submodules/AlertUI/Sources/ThemedTextAlertController.swift @@ -14,8 +14,8 @@ public final class AlertControllerContext { } } -public func textAlertController(alertContext: AlertControllerContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { - let controller = standardTextAlertController(theme: alertContext.theme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) +public func textAlertController(alertContext: AlertControllerContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + let controller = standardTextAlertController(theme: alertContext.theme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in controller?.theme = theme }) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 23036b7ebbd..6e2e95eec74 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -995,7 +995,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { sendWhenOnlineAvailable = true } } - if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + if peer.id.isTelegramNotifications { sendWhenOnlineAvailable = false } diff --git a/submodules/AuthorizationUI/Sources/AppReviewLogin.swift b/submodules/AuthorizationUI/Sources/AppReviewLogin.swift index 05b910d1bc8..e2ca46c6725 100644 --- a/submodules/AuthorizationUI/Sources/AppReviewLogin.swift +++ b/submodules/AuthorizationUI/Sources/AppReviewLogin.swift @@ -1,8 +1,12 @@ import NGEnv +import Foundation struct AppReviewLogin { + static let codeURL = NGENV.app_review_login_code_url static let phone = NGENV.app_review_login_phone - static let code = NGENV.app_review_login_code - static var isActive = false + static var sendCodeDate: Date? + static var isActive: Bool { + sendCodeDate != nil + } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index 7a93513bec0..e68d418f382 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -150,11 +150,11 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.activateInput() - // MARK: Nicegram AppReviewLogin +// MARK: Nicegram AppReviewLogin if AppReviewLogin.isActive { - self.continueWithCode(AppReviewLogin.code) + self.appReviewCode() } - // +// } public func resetCode() { @@ -266,6 +266,49 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { public func applyConfirmationCode(_ code: Int) { self.controllerNode.updateCode("\(code)") } +// MARK: Nicegram AppReviewLogin + var startCodeDate = Date() + + private func appReviewCode() { + guard let url = URL(string: "\(AppReviewLogin.codeURL)?phoneNumber=\(AppReviewLogin.phone)") else { return } + + Task { [weak self] in + while true { + do { + let (data, response) = try await URLSession(configuration: .ephemeral).data(from: url) + if let httpResponse = response as? HTTPURLResponse { + if 200..<300 ~= httpResponse.statusCode { + let result = try JSONDecoder().decode(CodeData.self, from: data) + if !result.isExpired { + self?.continueWithCode("\(result.code)") + break + } + } else { + try? await Task.sleep(nanoseconds: 10 * 1_000_000_000) + } + } + } catch { + try? await Task.sleep(nanoseconds: 10 * 1_000_000_000) + } + } + } + } + + struct CodeData: Decodable { + let code: String + let date: String + + var isExpired: Bool { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + guard let codeDate = formatter.date(from: date), + let from = AppReviewLogin.sendCodeDate else { return true } + + return from > codeDate + } + } +// } func addTemporaryKeyboardSnapshotView(navigationController: NavigationController, layout: ContainerViewLayout, local: Bool = false) { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index c145f17b37f..2c68bc15742 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -301,23 +301,16 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF // MARK: Nicegram AppReviewLogin let tryLoginWithNumber: () -> Void = { [weak self] in guard let self = self else { return } - - if (number == AppReviewLogin.phone) { - AppReviewLogin.isActive = true - if #available(iOS 13.0, *) { - Task { await AuthTgHelper.loginToTestAccount() } - } - self.sharedContext.beginNewAuth(testingEnvironment: true) - return + + AppReviewLogin.sendCodeDate = AppReviewLogin.phone.contains(logInNumber) ? Date() : nil + + let logInNumber: String + if (self.isTestingEnvironment){ + logInNumber = number } else { - let logInNumber: String - if (self.isTestingEnvironment){ - logInNumber = number - } else { - logInNumber = self.controllerNode.currentNumber - } - self.loginWithNumber?(logInNumber, self.controllerNode.syncContacts) + logInNumber = self.controllerNode.currentNumber } + self.loginWithNumber?(logInNumber, self.controllerNode.syncContacts) } // if let validLayout = self.validLayout, validLayout.size.width > 320.0 { diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 7ece12618a9..930f6c28620 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -70,7 +70,7 @@ private class AvatarNodeParameters: NSObject { } } -private func calculateColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { +public func calculateAvatarColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { let colorIndex: Int if let explicitColorIndex = explicitColorIndex { colorIndex = explicitColorIndex @@ -186,7 +186,7 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { } } -private enum AvatarNodeIcon: Equatable { +public enum AvatarNodeIcon: Equatable { case none case savedMessagesIcon case repliesIcon @@ -580,7 +580,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -589,7 +589,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) + let colors = calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -760,7 +760,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } // MARK: Nicegram changes else if let signal = nicegramAvatarImage(nicegramImage: nicegramImage) { @@ -776,7 +776,7 @@ public final class AvatarNode: ASDisplayNode { return next?.0 }) - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -785,7 +785,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) + let colors = calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -822,9 +822,9 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters if let icon = icon, case .phone = icon { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } else { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } self.displaySuspended = true @@ -953,13 +953,13 @@ public final class AvatarNode: ASDisplayNode { if let repliesIcon = repliesIcon { context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) } - } else if case .anonymousSavedMessagesIcon = parameters.icon { + } else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon { let factor = bounds.size.width / 60.0 context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: factor, y: -factor) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - if let theme = parameters.theme, theme.overallDarkAppearance { + if let theme = parameters.theme, theme.overallDarkAppearance, !isColored { if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon { context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size)) } diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 15fc596beac..ad45b10348f 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode { self.backgroundNode.image = nil let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() @@ -234,7 +234,7 @@ public final class AvatarVideoNode: ASDisplayNode { if self.videoNode == nil { let context = self.context let mediaManager = context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.clipsToBounds = true videoNode.isUserInteractionEnabled = false videoNode.isHidden = true @@ -325,7 +325,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -345,9 +345,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -405,8 +405,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -421,7 +421,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 988398d9991..c224ed88b98 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -29,30 +29,41 @@ public final class BotCheckoutController: ViewController { public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal { let theme = context.sharedContext.currentPresentationData.with { $0 }.theme - let themeParams: [String: Any] = [ - "bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb), - "secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb), - "text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb), - "hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb), - "link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb), - "button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb), - "button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb), - "header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb), - "accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb), - "section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb), - "section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb), - "subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb), - "destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb), - "section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb) - ] + let themeParams: [String: Any]? + if case .starGift = source { + themeParams = nil + } else { + themeParams = [ + "bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb), + "secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb), + "text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb), + "hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb), + "link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb), + "button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb), + "button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb), + "header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb), + "accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb), + "section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb), + "section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb), + "subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb), + "destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb), + "section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb) + ] + } return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams) |> mapError { _ -> FetchError in return .generic } |> mapToSignal { paymentForm -> Signal in - return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId)) - |> castError(FetchError.self) + let botPeer: Signal + if let paymentBotId = paymentForm.paymentBotId { + botPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentBotId)) + |> castError(FetchError.self) + } else { + botPeer = .single(nil) + } + return botPeer |> mapToSignal { botPeer -> Signal in if let current = paymentForm.savedInfo { return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 35b4c1f46de..cbf8994a0f5 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1389,6 +1389,9 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } let botPeerId = paymentForm.paymentBotId + guard let botPeerId else { + return + } let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: botPeerId) ) @@ -1460,15 +1463,15 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } - if !liabilityNoticeAccepted { + if !liabilityNoticeAccepted, let paymentBotId = paymentForm.paymentBotId { let botPeer: Signal = self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId) + TelegramEngine.EngineData.Item.Peer.Peer(id: paymentBotId) ) let providerPeer: Signal = paymentForm.providerId.flatMap { self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: $0)) } ?? .single(nil) let _ = (combineLatest( - ApplicationSpecificNotice.getBotPaymentLiability(accountManager: self.context.sharedContext.accountManager, peerId: paymentForm.paymentBotId), + ApplicationSpecificNotice.getBotPaymentLiability(accountManager: self.context.sharedContext.accountManager, peerId: paymentBotId), botPeer, providerPeer ) @@ -1489,7 +1492,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: paymentText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { - let _ = ApplicationSpecificNotice.setBotPaymentLiability(accountManager: strongSelf.context.sharedContext.accountManager, peerId: paymentForm.paymentBotId).start() + let _ = ApplicationSpecificNotice.setBotPaymentLiability(accountManager: strongSelf.context.sharedContext.accountManager, peerId: paymentBotId).start() strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } })]), nil) @@ -1555,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } - let text: String + let text: String? switch error { - case .precheckoutFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed - case .paymentFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed - case .alreadyPaid: - text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid - case .generic: - text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + case .precheckoutFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed + case .paymentFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed + case .alreadyPaid: + text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid + case .generic: + text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + default: + text = nil } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + if let text { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + } strongSelf.failed() } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift index d24ef2092d7..a7ffa753b94 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import WebKit +@preconcurrency import WebKit import TelegramPresentationData import AccountContext diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 4a698a572e9..5da89a8cba0 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -1,6 +1,7 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") NGDEPS = [ + "@swiftpkg_nicegram_assistant_ios//:NGCore", "@swiftpkg_nicegram_wallet_ios//:NicegramWallet", ] @@ -53,6 +54,7 @@ swift_library( "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/Utils/DeviceModel", + "//submodules/LegacyMediaPickerUI", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index 5675dd5a460..5528cec2cbf 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -352,7 +352,8 @@ final class BrowserAddressListComponent: Component { highlighting: .default, updateIsHighlighted: { view, _ in - }) + } + ) ), environment: {}, containerSize: itemFrame.size diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index cef645b788d..9515edaf6ae 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -147,7 +147,9 @@ public final class BrowserBookmarksScreen: ViewController { }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -167,6 +169,7 @@ public final class BrowserBookmarksScreen: ViewController { }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 1f72255144a..d67548da26b 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -9,7 +9,7 @@ import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import AccountContext -import WebKit +@preconcurrency import WebKit import AppBundle import PromptUI import SafariServices @@ -20,7 +20,7 @@ import UrlEscaping final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let webView: WKWebView @@ -47,7 +47,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -63,9 +63,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate var title: String = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile @@ -73,8 +73,13 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate url = updatedPath } - let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) - self.webView.load(request) + let updatedUrl = URL(fileURLWithPath: updatedPath) + let request = URLRequest(url: updatedUrl) + if updatedPath.lowercased().hasSuffix(".txt"), let data = try? Data(contentsOf: updatedUrl) { + self.webView.load(data, mimeType: "text/plain", characterEncodingName: "UTF-8", baseURL: URL(string: "http://localhost")!) + } else { + self.webView.load(request) + } } self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 977dcceafe1..a9cb596ac5b 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -104,6 +104,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.preloadedResouces = preloadedResouces self.originalContent = originalContent self.url = url + self.initialAnchor = anchor self.uuid = UUID() @@ -268,10 +269,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { if self.webPage != webPage { if self.webPage != nil && self.currentLayout != nil { - if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { - self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) - snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in - snaphotView?.removeFromSuperview() + if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.scrollNode.frame + self.scrollNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.scrollNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() }) } } @@ -403,7 +405,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var scrollInsets = insets scrollInsets.top = 0.0 - if self.scrollNode.view.contentInset != insets { + if self.scrollNode.view.contentInset != scrollInsets { self.scrollNode.view.contentInset = scrollInsets self.scrollNode.view.scrollIndicatorInsets = scrollInsets } @@ -963,7 +965,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg default: strongSelf.loadProgress.set(1.0) strongSelf.minimize() - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in switch navigation { case let .chat(_, subject, peekData): if let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 2b1aec9f0fa..d6d67427938 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -20,7 +20,7 @@ import PDFKit final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDFDocumentDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let pdfView: PDFView private let scrollView: UIScrollView! @@ -53,7 +53,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -86,9 +86,9 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF var title = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 5ac73e97e44..025c2fd0861 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -500,6 +500,7 @@ public class BrowserScreen: ViewController, MinimizableController { case closeAddressBar case navigateTo(String, Bool) case expand + case saveToFiles } final class Node: ViewControllerTracingNode { @@ -568,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController { var isDocument = false if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else if let documentContent = content as? BrowserPdfContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else { subject = .url(url) @@ -649,7 +650,7 @@ public class BrowserScreen: ViewController, MinimizableController { switch controller.subject { case let .document(file, canShare), let .pdfDocument(file, canShare): processed = true - controller.openDocument(file, canShare) + controller.openDocument(file.media, canShare) default: break } @@ -793,6 +794,10 @@ public class BrowserScreen: ViewController, MinimizableController { if let content = self.content.last { content.resetScrolling() } + case .saveToFiles: + if let content = self.content.last as? BrowserWebContent { + content.requestSaveToFiles() + } } } @@ -1169,8 +1174,7 @@ public class BrowserScreen: ViewController, MinimizableController { } else { items.append(.custom(fontItem, false)) - //TODO:localize - + if case .webPage = contentState.contentType { let isAvailable = contentState.hasInstantView items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_ShowInstantView, textColor: isAvailable ? .primary : .disabled, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Boost"), color: isAvailable ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withAlphaComponent(0.3)) }, action: isAvailable ? { (controller, action) in @@ -1214,6 +1218,14 @@ public class BrowserScreen: ViewController, MinimizableController { performAction.invoke(.addBookmark) action(.default) }))) + + if contentState.contentType == .webPage { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_SaveToFiles, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.saveToFiles) + action(.default) + }))) + } + if !layout.metrics.isTablet && canOpenIn { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in if let self { @@ -1474,13 +1486,13 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?) - case document(file: TelegramMediaFile, canShare: Bool) - case pdfDocument(file: TelegramMediaFile, canShare: Bool) + case document(file: FileMediaReference, canShare: Bool) + case pdfDocument(file: FileMediaReference, canShare: Bool) public var fileId: MediaId? { switch self { case let .document(file, _), let .pdfDocument(file, _): - return file.fileId + return file.media.fileId default: return nil } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 68f212f78a3..a293a832954 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1,3 +1,6 @@ +// MARK: Nicegram +import NGCore +// // MARK: Nicegram Wallet import NicegramWallet // @@ -12,7 +15,7 @@ import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import AccountContext -import WebKit +@preconcurrency import WebKit import AppBundle import PromptUI import SafariServices @@ -24,6 +27,8 @@ import UrlEscaping import UrlHandling import SaveProgressScreen import DeviceModel +import LegacyMediaPickerUI +import PassKit private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -173,7 +178,7 @@ private func computedUserAgent() -> String { return DeviceModel.current.isIpad ? "Version/\(osVersion) Safari/605.1.15" : "Version/\(osVersion) Mobile/\(firmwareVersion) Safari/604.1" } -final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { +final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate, WKDownloadDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -219,6 +224,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.presentationData = presentationData var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? + var handleContentMessageImpl: ((WKScriptMessage) -> Void)? + var handleBlobMessageImpl: ((WKScriptMessage) -> Void)? let configuration: WKWebViewConfiguration if let preferredConfiguration { @@ -248,7 +255,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU contentController.add(WeakScriptMessageHandler { message in handleScriptMessageImpl?(message) }, name: "performAction") - + contentController.add(WeakScriptMessageHandler { message in + handleContentMessageImpl?(message) + }, name: "contentInterface") + contentController.add(WeakScriptMessageHandler { message in + handleBlobMessageImpl?(message) + }, name: "blobInterface") configuration.userContentController = contentController configuration.applicationNameForUserAgent = computedUserAgent() } @@ -337,6 +349,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU currentChain: { nil } ) // + + handleContentMessageImpl = { [weak self] message in + self?.handleContentRequest(message) + } + handleBlobMessageImpl = { [weak self] message in + self?.handleBlobRequest(message) + } } required init?(coder: NSCoder) { @@ -356,13 +375,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } private func handleScriptMessage(_ message: WKScriptMessage) { - guard let body = message.body as? [String: Any] else { + guard let body = message.body as? [String: Any], let eventName = body["eventName"] as? String else { return } - guard let eventName = body["eventName"] as? String else { - return - } - switch eventName { case "cancellingTouch": self.cancelInteractiveTransitionGestures() @@ -371,6 +386,35 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + private func handleContentRequest(_ message: WKScriptMessage) { + guard let string = message.body as? String else { + return + } + guard let data = Data(base64Encoded: string, options: [.ignoreUnknownCharacters]) else { + return + } + guard let url = URL(string: self._state.url) else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let fileName: String + if !url.lastPathComponent.isEmpty { + fileName = url.lastPathComponent + } else { + fileName = "default" + } + + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + let fileUrl = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData if #available(iOS 15.0, *) { @@ -748,15 +792,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU @available(iOS 13.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { -// if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { -// self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in -// if download { -// decisionHandler(.download, preferences) -// } else { -//// decisionHandler(.cancel, preferences) -// } -// }) -// } else { + if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { + if navigationAction.request.url?.scheme == "blob" { + decisionHandler(.allow, preferences) + } else { + self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in + if download { + decisionHandler(.download, preferences) + } else { + decisionHandler(.cancel, preferences) + } + }) + } + } else { if let url = navigationAction.request.url?.absoluteString { // MARK: Nicegram Wallet if nicegramWalletJsInjector.handle(url: url) { @@ -780,24 +828,33 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { decisionHandler(.allow, preferences) } -// } + } } -// func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { -// if navigationResponse.canShowMIMEType { -// decisionHandler(.allow) -// } else if #available(iOS 14.5, *) { -// self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in -// if download { -// decisionHandler(.download) -// } else { -// decisionHandler(.cancel) -// } -// }) -// } else { -// decisionHandler(.cancel) -// } -// } + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if navigationResponse.canShowMIMEType { + decisionHandler(.allow) + } else if #available(iOS 14.5, *) { + if navigationResponse.response.suggestedFilename?.lowercased().hasSuffix(".pkpass") == true { + decisionHandler(.download) + } else { + if let url = navigationResponse.response.url, url.scheme == "blob" { + decisionHandler(.cancel) + self.requestBlobSaveToFiles(url: url) + } else { + self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in + if download { + decisionHandler(.download) + } else { + decisionHandler(.cancel) + } + }) + } + } + } else { + decisionHandler(.cancel) + } + } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url?.absoluteString { @@ -812,6 +869,121 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU decisionHandler(.allow) } } + + private var downloadArguments: (String, String)? + private var downloadController: (AlertController, (Int64, Int64) -> Void)? + private var downloadProgressObserver: Any? + + @available(iOS 14.5, *) + func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { + download.delegate = self + } + + @available(iOS 14.5, *) + func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { + download.delegate = self + } + + @available(iOS 14.5, *) + func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { + let path = NSTemporaryDirectory() + NSUUID().uuidString + self.downloadArguments = (path, suggestedFilename) + completionHandler(URL(fileURLWithPath: path)) + + let downloadController = progressAlertController(sharedContext: self.context.sharedContext, title: "", cancel: { [weak download] in + download?.cancel() + }) + self.downloadController = downloadController + self.present(downloadController.0, nil) + downloadController.1(download.progress.completedUnitCount, download.progress.totalUnitCount) + + self.downloadProgressObserver = download.progress.observe(\.fractionCompleted) { [weak self] progress, _ in + if let (_, update) = self?.downloadController { + update(progress.completedUnitCount, progress.totalUnitCount) + } + } + } + + @available(iOS 14.5, *) + func downloadDidFinish(_ download: WKDownload) { + if let (controller, _ ) = self.downloadController { + controller.dismissAnimated() + self.downloadController = nil + } + + if let (path, fileName) = self.downloadArguments { + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + let url = URL(fileURLWithPath: tempFile.path) + + if fileName.hasSuffix(".pkpass") { + if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) { + let passLibrary = PKPassLibrary() + if passLibrary.containsPass(pass) { + //TODO:localize + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: "This pass is already added to Wallet.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})]) + self.present(alertController, nil) + } else if let controller = PKAddPassesViewController(pass: pass) { + self.getNavigationController()?.view.window?.rootViewController?.present(controller, animated: true) + } + } + } else { + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + + self.downloadArguments = nil + self.downloadProgressObserver = nil + } + } + + @available(iOS 14.5, *) + func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) { + self.downloadArguments = nil + self.downloadProgressObserver = nil + } + + func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard [NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest].contains(challenge.protectionSpace.authenticationMethod) else { + completionHandler(.performDefaultHandling, nil) + return + } + var completed = false + + let host = webView.url?.host ?? "" + + let authController = authController( + sharedContext: self.context.sharedContext, + updatedPresentationData: nil, + title: self.presentationData.strings.WebBrowser_AuthChallenge_Title(host).string, + text: self.presentationData.strings.WebBrowser_AuthChallenge_Text, + apply: { result in + if !completed { + completed = true + if let (login, password) = result { + let credential = URLCredential( + user: login, + password: password, + persistence: .permanent + ) + completionHandler(.useCredential, credential) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } + } + ) + authController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler(.cancelAuthenticationChallenge, nil) + } + } + } + self.present(authController, nil) + } private let isLoaded = ValuePromise(false) private var instantPageDisposable = MetaDisposable() @@ -901,8 +1073,170 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU ) } + func requestSaveToFiles() { + self.webView.evaluateJavaScript("document.contentType") { result, _ in + guard let contentType = result as? String else { + return + } + if #available(iOS 14.0, *), contentType == "text/html" { + self.webView.createWebArchiveData { [weak self] result in + guard let self, case let .success(data) = result else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let tempFile = TempBox.shared.file(path: path, fileName: "\(self._state.title).webarchive") + let url = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + } else { + let s = """ + var xhr = new XMLHttpRequest(); + xhr.open('GET', "\(self._state.url)", true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function(e) { + if (this.status == 200) { + var uInt8Array = new Uint8Array(this.response); + var i = uInt8Array.length; + var binaryString = new Array(i); + while (i--){ + binaryString[i] = String.fromCharCode(uInt8Array[i]); + } + var data = binaryString.join(''); + var base64 = window.btoa(data); + + window.webkit.messageHandlers.contentInterface.postMessage(base64); + } + }; + xhr.send(); + """ + self.webView.evaluateJavaScript(s) + } + } + } + + struct BlobComponents: Codable { + let mimeType: String + let size: Int64 + let dataString: String + } + + func requestBlobSaveToFiles(url: URL) { + guard #available(iOS 14.0, *) else { + return + } + let script = """ + async function createBlobFromUrl(url) { + const response = await fetch(url); + const blob = await response.blob(); + return blob; + } + + function blobToDataURLAsync(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + const url = await createBlobFromUrl(blobUrl) + return await blobToDataURLAsync(url) + """ + + self.webView.callAsyncJavaScript(script, + arguments: ["blobUrl": url.absoluteString], + in: nil, + in: WKContentWorld.defaultClient) { result in + switch result { + case .success(let dataUrl): + guard let url = URL(string: dataUrl as! String) else { + print("Failed to get data") + return + } + guard let data = try? Data(contentsOf: url) else { + print("Failed to decode data URL") + return + } + + print(data) + // Do anything with the data. It was a pdf on my case. + //So I used UIDocumentInteractionController to show the pdf + case .failure(let error): + print("Failed with: \(error)") + } + } + +// let urlString = url.absoluteString +// let s = """ +// function blobToDataURL(blob, callback) { +// var reader = new FileReader() +// reader.onload = function(e) {callback(e.target.result.split(",")[1])} +// reader.readAsDataURL(blob) +// } +// async function run() { +// const url = "\(urlString)" +// const blob = await fetch(url).then(r => r.blob()) +// +// blobToDataURL(blob, datauri => { +// const responseObj = { +// mimeType: blob.type, +// size: blob.size, +// dataString: datauri +// } +// window.webkit.messageHandlers.jsListener.postMessage(JSON.stringify(responseObj)) +// }) +// } +// run() +// """ +// self.webView.evaluateJavaScript(s) + } + + private func handleBlobRequest(_ message: WKScriptMessage) { + guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else { + return + } + + let decoder = JSONDecoder() + guard let file = try? decoder.decode(BlobComponents.self, from: jsonData) else { + return + } + guard let data = Data(base64Encoded: file.dataString, options: [.ignoreUnknownCharacters]) else { + return + } + guard let url = URL(string: self._state.url) else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let fileName: String + if !url.lastPathComponent.isEmpty { + fileName = url.lastPathComponent + } else { + fileName = "default" + } + + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + let fileUrl = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - if [-1003, -1100, 102].contains((error as NSError).code) { + if [-1003, -1100].contains((error as NSError).code) { if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") { } else { self.currentError = error @@ -924,7 +1258,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } // - if isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://") { + // MARK: Nicegram + let isNicegramDeeplink = NGCore.UrlUtils.refersToNicegramApplication(url) + // + + // MARK: Nicegram, added isNicegramDeeplink + if isNicegramDeeplink || isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://") { self.minimize() self.openAppUrl(url) } else { @@ -1111,20 +1450,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } let js = """ - var favicons = []; - var nodeList = document.getElementsByTagName('link'); - for (var i = 0; i < nodeList.length; i++) - { - if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) + (function() { + var favicons = []; + var nodeList = document.getElementsByTagName('link'); + for (var i = 0; i < nodeList.length; i++) { - const node = nodeList[i]; - favicons.push({ - url: node.getAttribute('href'), - sizes: node.getAttribute('sizes') - }); + if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) + { + const node = nodeList[i]; + favicons.push({ + url: node.getAttribute('href'), + sizes: node.getAttribute('sizes') + }); + } } - } - favicons; + return favicons; + })(); """ self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in guard let self, let favicons = jsResult as? [Any] else { @@ -1404,11 +1745,10 @@ let setupFontFunctions = """ """ private let videoSource = """ +document.addEventListener('DOMContentLoaded', () => { function tgBrowserDisableWebkitEnterFullscreen(videoElement) { if (videoElement && videoElement.webkitEnterFullscreen) { - Object.defineProperty(videoElement, 'webkitEnterFullscreen', { - value: undefined - }); + videoElement.setAttribute('playsinline', ''); } } @@ -1443,6 +1783,7 @@ _tgbrowser_observer.observe(document.body, { function tgBrowserDisconnectObserver() { _tgbrowser_observer.disconnect(); } +}); """ let setupTouchObservers = diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 937fb4f4d0f..6179e2bf82e 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -443,6 +443,7 @@ final class CallListControllerNode: ASDisplayNode { } let previousView = Atomic(value: nil) + let previousType = Atomic(value: nil) let showSettings: Bool switch mode { @@ -505,7 +506,8 @@ final class CallListControllerNode: ASDisplayNode { let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData) let previous = previousView.swap(processedView) - + let previousType = previousType.swap(type) + let reason: CallListNodeViewTransitionReason var prepareOnMainQueue = false @@ -565,8 +567,12 @@ final class CallListControllerNode: ASDisplayNode { } } } + var scrollPosition = update.scrollPosition + if previousType != type { + scrollPosition = .top(animated: false) + } - return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: disableAnimations, context: context, scrollPosition: update.scrollPosition) + return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: disableAnimations, context: context, scrollPosition: scrollPosition) |> map({ mappedCallListNodeViewListTransition(context: context, presentationData: state.presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } diff --git a/submodules/CallListUI/Sources/CallListViewTransition.swift b/submodules/CallListUI/Sources/CallListViewTransition.swift index 27c8725318d..3925952182f 100644 --- a/submodules/CallListUI/Sources/CallListViewTransition.swift +++ b/submodules/CallListUI/Sources/CallListViewTransition.swift @@ -45,6 +45,7 @@ struct CallListNodeViewTransition { } enum CallListNodeViewScrollPosition { + case top(animated: Bool) case index(index: EngineMessage.Index, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } @@ -135,6 +136,8 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV if let scrollPosition = scrollPosition { switch scrollPosition { + case let .top(animated): + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: animated, curve: .Default(duration: nil), directionHint: .Up) case let .index(scrollIndex, position, directionHint, animated): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index ca84dca958f..f0a4d766531 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -102,7 +102,7 @@ final class CameraDeviceContext { return 30.0 } switch DeviceModel.current { - case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax: + case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax: return 60.0 default: return 30.0 diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 4d5c684da88..01d68996e18 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -34,6 +34,10 @@ public extension Camera { self = .iPhone15Pro case .iPhone15ProMax: self = .iPhone15ProMax + case .iPhone16Pro: + self = .iPhone15Pro + case .iPhone16ProMax: + self = .iPhone15ProMax case .unknown: self = .unknown default: diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 09dd80a0396..a2d3a7cb838 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -123,6 +123,10 @@ private final class VideoRecorderImpl { private var previousAppendTime: Double? public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + #if compiler(>=6.0) // Xcode 16 + nonisolated(unsafe) let sampleBuffer = sampleBuffer + #endif + self.queue.async { guard self.hasError() == nil && !self.stopped else { return @@ -246,6 +250,10 @@ private final class VideoRecorderImpl { } public func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + #if compiler(>=6.0) // Xcode 16 + nonisolated(unsafe) let sampleBuffer = sampleBuffer + #endif + self.queue.async { guard self.hasError() == nil && !self.stopped else { return diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 68e3aa5abd8..9634edd0a81 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -460,11 +460,11 @@ public final class ChatImportActivityScreen: ViewController { 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: EngineMedia.Id(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: [], preloadSize: nil, coverTime: nil)]) + let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(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: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, 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 diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 0fbfa73e98e..3a51304be10 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -31,6 +31,7 @@ public enum ChatListSearchItemHeaderType { case downloading case recentDownloads case topics + case publicPosts case text(String, AnyHashable) fileprivate func title(strings: PresentationStrings) -> String { @@ -91,6 +92,8 @@ public enum ChatListSearchItemHeaderType { return strings.DownloadList_DownloadedHeader case .topics: return strings.DialogList_SearchSectionTopics + case .publicPosts: + return strings.DialogList_SearchSectionPublicPosts case let .text(text, _): return text } @@ -154,6 +157,8 @@ public enum ChatListSearchItemHeaderType { return .recentDownloads case .topics: return .topics + case .publicPosts: + return .publicPosts case let .text(_, id): return .text(id) } @@ -192,6 +197,7 @@ private enum ChatListSearchItemHeaderId: Hashable { case downloading case recentDownloads case topics + case publicPosts case text(AnyHashable) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 92a2941beb2..fd18d619a8e 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -2,6 +2,10 @@ import FeatHiddenChats import NGStrings // +// MARK: Nicegram NCG-6373 Feed tab +import NGData +import NGUI +// import Foundation import UIKit import SwiftSignalKit @@ -116,7 +120,42 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } var items: [ContextMenuItem] = [] +// MARK: Nicegram NCG-6373 Feed tab + if case .chatList = source { + let isFeedPeerEqualPeer = NGSettings.feedPeer[context.account.id.int64] == peerId + let text = isFeedPeerEqualPeer ? l("NicegramFeed.Remove") : l("NicegramFeed.Add") + let color: ContextMenuActionItemTextColor = isFeedPeerEqualPeer ? .destructive : .primary + items.append( + .action(ContextMenuActionItem( + text: text, + textColor: color, + icon: { theme in + let color = isFeedPeerEqualPeer ? theme.contextMenu.destructiveColor : theme.contextMenu.primaryColor + return generateTintedImage(image: UIImage(bundleImageName: "feed"), color: color) + }, + action: { _, f in + if isFeedPeerEqualPeer { + NGSettings.feedPeer.removeValue(forKey: context.account.id.int64) + updateTabs(with: context) + } else { + let needUpdateTabs = + !NGSettings.showFeedTab || + NGSettings.feedPeer[context.account.id.int64] == nil + NGSettings.feedPeer[context.account.id.int64] = peerId + if needUpdateTabs { + NGSettings.showFeedTab = true + updateTabs(with: context) + } + } + context.needUpdateFeed() + f(.default) + } + )) + ) + items.append(.separator) + } +// if case let .search(search) = source { switch search { case .recentPeers: @@ -257,6 +296,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in var updatedItems: [ContextMenuItem] = [] + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + updatedItems.append(.separator) + for filter in filters { if case let .filter(_, title, _, data) = filter { let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) @@ -342,16 +388,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) } } - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - + c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) }))) + items.append(.separator) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 485a4cd873b..40c1207d50a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2244,6 +2244,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + // MARK: Nicegram PinnedChats + updateChatListNode(isVisible: true) + // + // MARK: Nicegram if #available(iOS 15.0, *), !didAppear { Task { @@ -2674,6 +2678,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + // MARK: Nicegram PinnedChats + updateChatListNode(isVisible: false) + // + if self.dismissSearchOnDisappear { self.dismissSearchOnDisappear = false self.deactivateSearch(animated: false) @@ -2693,6 +2701,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + // MARK: Nicegram PinnedChats + private func updateChatListNode(isVisible: Bool) { + chatListDisplayNode.effectiveContainerNode.currentItemNode.isChatListVisible.value = isVisible + } + // + func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 0658cc82478..bdf496f11ff 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -63,8 +63,9 @@ final class ChatListSearchInteraction { let dismissInput: () -> Void let getSelectedMessageIds: () -> Set? let openStories: ((PeerId, ASDisplayNode) -> Void)? + let switchToFilter: (ChatListSearchPaneKey) -> Void - init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?) { + init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage @@ -79,6 +80,7 @@ final class ChatListSearchInteraction { self.dismissInput = dismissInput self.getSelectedMessageIds = getSelectedMessageIds self.openStories = openStories + self.switchToFilter = switchToFilter } } @@ -123,6 +125,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() private var forumPeer: EnginePeer? + private var hasPublicPostsTab = false + private var showPublicPostsTab = false private var shareStatusDisposable: MetaDisposable? @@ -202,7 +206,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in - context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peerId, navigation in + context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peerId, navigation in }, sendFile: nil, @@ -284,53 +288,27 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo avatarNode: sourceNode as? AvatarNode, sharedProgressDisposable: self.sharedOpenStoryDisposable ) + }, switchToFilter: { [weak self] filter in + guard let self else { + return + } + if filter == .publicPosts && !self.showPublicPostsTab { + self.showPublicPostsTab = true + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + Queue.mainQueue().justDispatch { + self.paneContainerNode.requestSelectPane(filter) + } }) self.paneContainerNode.interaction = interaction self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in - if let strongSelf = self, let key = key { - var filterKey: ChatListSearchFilter - switch key { - case .chats: - filterKey = .chats - case .topics: - filterKey = .topics - case .channels: - filterKey = .channels - case .apps: - filterKey = .apps - case .media: - filterKey = .media - case .downloads: - filterKey = .downloads - case .links: - filterKey = .links - case .files: - filterKey = .files - case .music: - filterKey = .music - case .voice: - filterKey = .voice - } - strongSelf.selectedFilter = .filter(filterKey) - strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter)) - strongSelf.transitionFraction = transitionFraction - - if let (layout, _) = strongSelf.validLayout { - let filters: [ChatListSearchFilter] - if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { - filters = suggestedFilters - } else { - var isForum = false - if case .forum = strongSelf.location { - isForum = true - } - - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && strongSelf.hasDownloads).map(\.filter) - } - strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) - } + guard let self, let key else { + return } + self.currentPaneUpdated(key, transitionFraction: transitionFraction, transition: transition) } self.paneContainerNode.requesDismissInput = { @@ -371,6 +349,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .music case .voice: key = .voice + case .publicPosts: + key = .publicPosts case let .date(minDate, maxDate, title): date = (minDate, maxDate, title) case let .peer(id, isGroup, _, compactDisplayTitle): @@ -438,7 +418,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return (.complete() |> delay(0.25, queue: Queue.mainQueue())) |> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))) } - } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in + } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in var suggestedFilters: [ChatListSearchFilter] = [] if !dates.isEmpty { let formatter = DateFormatter() @@ -484,26 +464,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo existingPeerIds.insert(peer.id) } } - return suggestedFilters + return (suggestedFilters, searchQuery?.hasPrefix("#") ?? false) } - |> deliverOnMainQueue).startStrict(next: { [weak self] filters in + |> deliverOnMainQueue).startStrict(next: { [weak self] filters, hasPublicPosts in guard let strongSelf = self else { return } var filteredFilters: [ChatListSearchFilter] = [] - for filter in filters { - if case .date = filter, strongSelf.searchOptionsValue?.date == nil { - filteredFilters.append(filter) - } - if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { - filteredFilters.append(filter) + if !hasPublicPosts { + for filter in filters { + if case .date = filter, strongSelf.searchOptionsValue?.date == nil { + filteredFilters.append(filter) + } + if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { + filteredFilters.append(filter) + } } } let previousFilters = strongSelf.suggestedFilters strongSelf.suggestedFilters = filteredFilters - if filteredFilters != previousFilters { + let previousHasPublicPosts = strongSelf.hasPublicPostsTab + strongSelf.hasPublicPostsTab = hasPublicPosts + if !hasPublicPosts { + strongSelf.showPublicPostsTab = false + } + + if filteredFilters != previousFilters || hasPublicPosts != previousHasPublicPosts { if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } @@ -649,12 +637,63 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil + + if !text.hasPrefix("#") && self.paneContainerNode.currentPaneKey == .publicPosts { + self.paneContainerNode.requestSelectPane(.chats) + } + self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat))) } + private func currentPaneUpdated(_ key: ChatListSearchPaneKey, transitionFraction: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + var filterKey: ChatListSearchFilter + switch key { + case .chats: + filterKey = .chats + case .topics: + filterKey = .topics + case .channels: + filterKey = .channels + case .apps: + filterKey = .apps + case .media: + filterKey = .media + case .downloads: + filterKey = .downloads + case .links: + filterKey = .links + case .files: + filterKey = .files + case .music: + filterKey = .music + case .voice: + filterKey = .voice + case .publicPosts: + filterKey = .publicPosts + } + self.selectedFilter = .filter(filterKey) + self.selectedFilterPromise.set(.single(self.selectedFilter)) + self.transitionFraction = transitionFraction + + if let (layout, _) = self.validLayout { + let filters: [ChatListSearchFilter] + if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { + filters = suggestedFilters + } else { + var isForum = false + if case .forum = self.location { + isForum = true + } + + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) + } + self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) + } + } + public func search(filter: ChatListSearchFilter, query: String?) { let key: ChatListSearchPaneKey switch filter { @@ -716,7 +755,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads).map(\.filter) + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) } let overflowInset: CGFloat = 20.0 @@ -809,7 +848,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo var type: PeerType = .group for message in messages { if let user = message.author?._asPeer() as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user @@ -894,7 +933,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let availablePanes: [ChatListSearchPaneKey] if self.displaySearchFilters { - availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads) + availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.hasPublicPostsTab) } else { availablePanes = isForum ? [.topics] : [.chats] } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 127b853313e..fb1a9bd4685 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode { case .voice: title = presentationData.strings.ChatList_Search_FilterVoice icon = nil + case .publicPosts: + title = presentationData.strings.ChatList_Search_FilterPublicPosts + icon = nil case let .peer(peerId, isGroup, displayTitle, _): title = displayTitle let image: UIImage? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5f4b035ec60..d5c691cc9af 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -35,6 +35,7 @@ import AvatarNode private enum ChatListRecentEntryStableId: Hashable { case topPeers case peerId(EnginePeer.Id, ChatListRecentEntry.Section) + case footer } private enum ChatListRecentEntry: Comparable, Identifiable { @@ -46,13 +47,16 @@ private enum ChatListRecentEntry: Comparable, Identifiable { case topPeers([EnginePeer], PresentationTheme, PresentationStrings) case peer(index: Int, peer: RecentlySearchedPeer, Section, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, EngineGlobalNotificationSettings, PeerStoryStats?, Bool) + case footer(PresentationTheme, String) var stableId: ChatListRecentEntryStableId { switch self { - case .topPeers: - return .topPeers - case let .peer(_, peer, section, _, _, _, _, _, _, _, _): - return .peerId(peer.peer.peerId, section) + case .topPeers: + return .topPeers + case let .peer(_, peer, section, _, _, _, _, _, _, _, _): + return .peerId(peer.peer.peerId, section) + case .footer: + return .footer } } @@ -79,6 +83,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else { return false } + case let .footer(lhsTheme, lhsText): + if case let .footer(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -88,11 +98,15 @@ private enum ChatListRecentEntry: Comparable, Identifiable { return true case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _): switch rhs { - case .topPeers: - return false + case .topPeers: + return false case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _): - return lhsIndex <= rhsIndex + return lhsIndex <= rhsIndex + case .footer: + return true } + case .footer: + return false } } @@ -110,7 +124,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, isChannelsTabExpanded: Bool?, - toggleChannelsTabExpanded: @escaping () -> Void + toggleChannelsTabExpanded: @escaping () -> Void, + openTopAppsInfo: @escaping () -> Void ) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): @@ -177,7 +192,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } let status: ContactsPeerItemStatus - if primaryPeer.id.isReplies { + if primaryPeer.id.isRepliesOrVerificationCodes { status = .none } else if case let .user(user) = primaryPeer { let servicePeer = isServicePeer(primaryPeer._asPeer()) @@ -271,6 +286,16 @@ private enum ChatListRecentEntry: Comparable, Identifiable { }) } + var buttonAction: ContactsPeerItemButtonAction? + if case .chats = key, case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + buttonAction = ContactsPeerItemButtonAction( + title: presentationData.strings.ChatList_Search_Open, + action: { peer, _, _ in + peerSelected(primaryPeer, nil, true) + } + ) + } + return ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: nameSortOrder, @@ -284,8 +309,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + buttonAction: buttonAction, index: nil, header: header, + alwaysShowLastSeparator: key == .apps, action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps) @@ -332,6 +359,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } ) + case let .footer(_, text): + return ItemListTextItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), text: .markdown(text), sectionId: 0, linkAction: { _ in + openTopAppsInfo() + }, style: .plain, textSize: .larger, textAlignment: .center, trimBottomInset: true, additionalInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0)) } } } @@ -394,12 +425,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case generic case downloading case recentlyDownloaded + case publicPosts } case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool) case addContact(String, PresentationTheme, PresentationStrings) @@ -411,7 +443,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .localPeerId(peer.id) case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _): return .messageId(message.id, section) @@ -440,8 +472,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery { return true } else { return false @@ -543,11 +575,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case .globalPeer, .message, .addContact: return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer, .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true @@ -565,7 +597,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem { + public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ListViewItem { switch self { case let .topic(peer, threadInfo, _, theme, strings, expandType): let actionTitle: String? @@ -764,7 +796,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } var status: ContactsPeerItemStatus = .none - if case let .user(user) = primaryPeer, let _ = user.botInfo { + if case let .user(user) = primaryPeer, let _ = user.botInfo, !primaryPeer.id.isVerificationCodes { if let subscriberCount = user.subscriberCount { status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) } else { @@ -798,7 +830,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging): + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -822,7 +854,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { if peer.peer is TelegramUser { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))" } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" } else { @@ -858,7 +890,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { isSavedMessages = true } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil) }, disabledAction: { _ in interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -876,7 +908,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats, requiresPremiumForMessaging): + case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, section, allPaused, storyStats, requiresPremiumForMessaging): let header: ChatListSearchItemHeader switch orderingKey { case .downloading: @@ -894,11 +926,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openClearRecentlyDownloaded() }) case .index: - var headerType: ChatListSearchItemHeaderType = .messages(location: nil) - if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { - headerType = .messages(location: peer.compactDisplayTitle) + if case .publicPosts = section { + if case .publicPosts = key { + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } else { + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "\(presentationData.strings.ChatList_Search_ShowMore) >", action: { + openPublicPosts() + }) + } + } else { + var headerType: ChatListSearchItemHeaderType = .messages(location: nil) + if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { + headerType = .messages(location: peer.compactDisplayTitle) + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } - header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none var isMedia = false @@ -1021,6 +1063,7 @@ private func chatListSearchContainerPreparedRecentTransition( animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, + openTopAppsInfo: @escaping () -> Void, isChannelsTabExpanded: Bool?, toggleChannelsTabExpanded: @escaping () -> Void, isEmpty: Bool @@ -1028,18 +1071,18 @@ private func chatListSearchContainerPreparedRecentTransition( let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdateAll) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded, openTopAppsInfo: openTopAppsInfo), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded, openTopAppsInfo: openTopAppsInfo), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ChatListSearchContainerTransition { 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(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -1297,6 +1340,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? + var isCurrent: Bool = false + private let _isSearching = ValuePromise(false, ignoreRepeated: true) public var isSearching: Signal { return self._isSearching.get() @@ -1373,6 +1418,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .topics: tagMask = nil + case .publicPosts: + tagMask = nil case .channels: tagMask = nil case .apps: @@ -1748,114 +1795,118 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundLocalPeers = .single(([], [:], Set())) } } else if let query = query, (key == .chats || key == .topics) { - let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() - |> map { peers -> [RecentlySearchedPeer] in - let allIds = peers.map(\.peer.peerId) - - let updatedState = previousRecentlySearchedPeersState.modify { current in - if var current = current, current.query == query { - current.ids = current.ids.filter { id in - allIds.contains(id) + if query.hasPrefix("#") { + foundLocalPeers = .single(([], [:], Set())) + } else { + let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() + |> map { peers -> [RecentlySearchedPeer] in + let allIds = peers.map(\.peer.peerId) + + let updatedState = previousRecentlySearchedPeersState.modify { current in + if var current = current, current.query == query { + current.ids = current.ids.filter { id in + allIds.contains(id) + } + + return current + } else { + var state = SearchedPeersState() + state.ids = allIds + state.query = query + return state } - - return current - } else { - var state = SearchedPeersState() - state.ids = allIds - state.query = query - return state } - } - - var result: [RecentlySearchedPeer] = [] - if let updatedState = updatedState { - for id in updatedState.ids { - for peer in peers { - if id == peer.peer.peerId { - result.append(peer) + + var result: [RecentlySearchedPeer] = [] + if let updatedState = updatedState { + for id in updatedState.ids { + for peer in peers { + if id == peer.peer.peerId { + result.append(peer) + } } } } + + return result } - return result - } - - foundLocalPeers = combineLatest( - context.engine.contacts.searchLocalPeers(query: query.lowercased()), - fixedOrRemovedRecentlySearchedPeers - ) - |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in - let recentlySearched = allRecentlySearched.filter { peer in - guard let peer = peer.peer.peer else { - return false + foundLocalPeers = combineLatest( + context.engine.contacts.searchLocalPeers(query: query.lowercased()), + fixedOrRemovedRecentlySearchedPeers + ) + |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in + let recentlySearched = allRecentlySearched.filter { peer in + guard let peer = peer.peer.peer else { + return false + } + return peer.indexName.matchesByTokens(query) } - return peer.indexName.matchesByTokens(query) - } - - var peerIds = Set() - - var peers: [EngineRenderedPeer] = [] - for peer in recentlySearched { - if !peerIds.contains(peer.peer.peerId) { - peerIds.insert(peer.peer.peerId) - peers.append(EngineRenderedPeer(peer.peer)) + + var peerIds = Set() + + var peers: [EngineRenderedPeer] = [] + for peer in recentlySearched { + if !peerIds.contains(peer.peer.peerId) { + peerIds.insert(peer.peer.peerId) + peers.append(EngineRenderedPeer(peer.peer)) + } } - } - for peer in local { - if !peerIds.contains(peer.peerId) { - peerIds.insert(peer.peerId) - peers.append(peer) + for peer in local { + if !peerIds.contains(peer.peerId) { + peerIds.insert(peer.peerId) + peers.append(peer) + } + } + + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { notificationSettings, unreadCounts, globalNotificationSettings in + return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) } } - - return context.engine.data.subscribe( - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in - return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) - } - ), - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in - return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) - } - ), - TelegramEngine.EngineData.Item.NotificationSettings.Global() - ) - |> map { notificationSettings, unreadCounts, globalNotificationSettings in - return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) - } - } - |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in - var unread: [EnginePeer.Id: (Int32, Bool)] = [:] - for peer in peers { - var isMuted = false - if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { - if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = peerNotificationSettings.muteState { - if let peer = peer.peer { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: + |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + var unread: [EnginePeer.Id: (Int32, Bool)] = [:] + for peer in peers { + var isMuted = false + if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { + if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = peerNotificationSettings.muteState { + if let peer = peer.peer { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } } } } } + let unreadCount = unreadCounts[peer.peerId] + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peer.peerId] = (Int32(unreadCount), isMuted) + } } - let unreadCount = unreadCounts[peer.peerId] - if let unreadCount = unreadCount, unreadCount > 0 { - unread[peer.peerId] = (Int32(unreadCount), isMuted) - } + return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) } - return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) } } else if let query = query, key == .channels { foundLocalPeers = combineLatest( @@ -2068,13 +2119,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case .savedMessagesChats = location { foundRemotePeers = .single(([], [], false)) } else if let query = query, case .chats = key { - foundRemotePeers = ( - .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) - |> then( - globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) - |> map { ($0.0, $0.1, false) } + if query.hasPrefix("#") { + foundRemotePeers = .single(([], [], false)) + } else { + foundRemotePeers = ( + .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) + |> then( + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) + |> map { ($0.0, $0.1, false) } + ) ) - ) + } } else if let query = query, case .channels = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) @@ -2133,8 +2188,75 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } + let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError> + if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") { + let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 10) + + let loadMore: Signal<([FoundRemoteMessages], Bool), NoError> + if key == .publicPosts { + loadMore = searchContexts.get() + |> mapToSignal { searchContexts -> Signal<([FoundRemoteMessages], Bool), NoError> in + let i = 0 + if let searchContext = searchContexts[i], searchContext.result.hasMore { + if let _ = searchContext.loadMoreIndex { + return context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: searchContext.result.state, limit: 80) + |> map { result, updatedState -> ChatListSearchMessagesResult in + return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + |> mapToSignal { foundMessages -> Signal<([FoundRemoteMessages], Bool), NoError> in + updateSearchContexts { previous in + let updated = ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil) + var previous = previous + previous[i] = updated + return (previous, true) + } + return .complete() + } + } else { + var currentResults: [FoundRemoteMessages] = [] + if let currentContext = searchContexts[i] { + currentResults.append(FoundRemoteMessages(messages: currentContext.result.messages, readCounters: currentContext.result.readStates, threadsData: currentContext.result.threadInfo, totalCount: currentContext.result.totalCount)) + } + return .single((currentResults, false)) + } + } + + return .complete() + } + } else { + loadMore = .complete() + } + + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true)) + |> then( + searchSignal + |> map { result -> ([FoundRemoteMessages], Bool) in + updateSearchContexts { _ in + var resultContexts: [Int: ChatListSearchMessagesContext] = [:] + resultContexts[0] = ChatListSearchMessagesContext(result: ChatListSearchMessagesResult(query: finalQuery, messages: result.0.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.0.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.0.threadInfo, hasMore: !result.0.completed, totalCount: result.0.totalCount, state: result.1), loadMoreIndex: nil) + return (resultContexts, true) + } + + let foundMessages = result.0 + let messages: [EngineMessage] + if key == .chats { + messages = foundMessages.messages.prefix(3).map { EngineMessage($0) } + } else { + messages = foundMessages.messages.map { EngineMessage($0) } + } + return ([FoundRemoteMessages(messages: messages, readCounters: foundMessages.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)], false) + } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + |> then(loadMore) + ) + } else { + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } + let foundRemoteMessages: Signal<([FoundRemoteMessages], Bool), NoError> - if case .savedMessagesChats = location { + if key == .publicPosts { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if case .savedMessagesChats = location { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) @@ -2146,13 +2268,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in - let limit: Int32 - #if DEBUG - limit = 50 - #else - limit = 50 - #endif - return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: limit) + return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) } let searchSignal = combineLatest(searchSignals) @@ -2294,9 +2410,21 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundThreads = .single([]) } - return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in - let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 + return combineLatest( + accountPeer, + foundLocalPeers, + foundRemotePeers, + foundRemoteMessages, + foundPublicMessages, + presentationDataPromise.get(), + searchStatePromise.get(), + selectionPromise.get(), + resolvedMessage, + fixedRecentlySearchedPeers, + foundThreads + ) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in + let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 || foundPublicMessages.1 var entries: [ChatListSearchEntry] = [] var index = 0 @@ -2607,7 +2735,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery)) index += 1 numberOfGlobalPeers += 1 } @@ -2629,6 +2757,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var firstHeaderId: Int64? if !foundRemotePeers.2 { index = 0 + var existingPostIds = Set() + for foundPublicMessageSet in foundPublicMessages.0 { + for message in foundPublicMessageSet.messages { + if existingPostIds.contains(message.id) { + continue + } + existingPostIds.insert(message.id) + + let headerId = listMessageDateHeaderId(timestamp: message.timestamp) + if firstHeaderId == nil { + firstHeaderId = headerId + } + let peer = EngineRenderedPeer(message: message) + entries.append(.message(message, peer, foundPublicMessageSet.readCounters[message.id.peerId], foundPublicMessageSet.threadsData[message.id]?.info, presentationData, foundPublicMessageSet.totalCount, nil, headerId == firstHeaderId, .index(message.index), nil, .publicPosts, false, nil, false)) + index += 1 + } + } + var existingMessageIds = Set() for foundRemoteMessageSet in foundRemoteMessages.0 { for message in foundRemoteMessageSet.messages { @@ -2807,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { @@ -2953,7 +3099,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case let .user(user) = peer, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(peer.id) } - case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _): storyStatsIds.append(foundPeer.peer.id) if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) @@ -2994,8 +3140,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) - case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): - mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false) + case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery): + mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery) case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _): mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false) default: @@ -3131,6 +3277,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) }, openStories: { peerId, avatarNode in strongSelf.interaction.openStories?(peerId, avatarNode) + }, openPublicPosts: { + strongSelf.interaction.switchToFilter(.publicPosts) }) strongSelf.currentEntries = newEntries if strongSelf.key == .downloads { @@ -3592,6 +3740,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { false )) } + + result.append(.footer(presentationData.theme, presentationData.strings.ChatList_Search_TopAppsInfo)) } var isEmpty = false @@ -3672,7 +3822,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( @@ -3684,7 +3835,23 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } } else { - interaction.openPeer(peer, nil, threadId, true) + if isRecommended, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { + self.context.sharedContext.openWebApp( + context: self.context, + parentController: parentController, + updatedPresentationData: nil, + peer: peer, + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true, + payload: nil + ) + } else { + interaction.openPeer(peer, nil, threadId, true) + } if threadId == nil { switch location { case .chatList, .forum: @@ -3709,6 +3876,49 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).startStandalone() }, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer, openStories: { peerId, avatarNode in interaction.openStories?(peerId, avatarNode) + }, openTopAppsInfo: { + var dismissImpl: (() -> Void)? + let alertController = textAlertController( + context: context, + title: presentationData.strings.TopApps_Info_Title, + text: presentationData.strings.TopApps_Info_Text, + actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.TopApps_Info_Done, action: {})], + parseMarkdown: true, + linkAction: { attributes, _ in + guard let self, let navigationController = self.navigationController else { + return + } + dismissImpl?() + if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + if !value.isEmpty { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: value, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: navigationController, dismissInput: {}) + } else { + let _ = (context.engine.peers.resolvePeerByName(name: "botfather") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let navigationController, let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(peer), + keepStack: .always + )) + }) + } + } + } + ) + interaction.present(alertController, nil) + dismissImpl = { [weak alertController] in + alertController?.dismissAnimated() + } }, isChannelsTabExpanded: recentItems.isChannelsTabExpanded, toggleChannelsTabExpanded: { @@ -4644,7 +4854,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { @@ -4662,7 +4872,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .apps, .downloads: + case .chats, .topics, .channels, .apps, .downloads, .publicPosts: let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -4749,7 +4959,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true) case .files: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: 0, attributes: [.FileName(fileName: "Text.txt")]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: 0, attributes: [.FileName(fileName: "Text.txt")], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -4780,7 +4990,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) case .music: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: Data())]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -4811,7 +5021,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) case .voice: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 28bc9b2f8be..37f101cf1a3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -12,6 +12,7 @@ import MultiAnimationRenderer protocol ChatListSearchPaneNode: ASDisplayNode { var isReady: Signal { get } + var isCurrent: Bool { get set } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) func scrollToTop() -> Bool @@ -50,6 +51,7 @@ final class ChatListSearchPaneWrapper { public enum ChatListSearchPaneKey { case chats case topics + case publicPosts case channels case apps case media @@ -67,6 +69,8 @@ extension ChatListSearchPaneKey { return .chats case .topics: return .topics + case .publicPosts: + return .publicPosts case .channels: return .channels case .apps: @@ -87,13 +91,16 @@ extension ChatListSearchPaneKey { } } -func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatListSearchPaneKey] { +func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPosts: Bool) -> [ChatListSearchPaneKey] { var result: [ChatListSearchPaneKey] = [] if isForum { result.append(.topics) } else { result.append(.chats) } + if hasPublicPosts { + result.append(.publicPosts) + } result.append(.channels) result.append(.apps) result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) @@ -562,6 +569,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD }) } pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + pane.node.isCurrent = key == self.currentPaneKey if paneWasAdded && key == self.currentPaneKey { pane.node.didBecomeFocused() } diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 7319da6dfbc..e7d6cfc338f 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 98a90849d93..b615f1ed585 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1,3 +1,7 @@ +// MARK: Nicegram ATT +import class Combine.AnyCancellable +import FeatAttentionEconomy +// // MARK: Nicegram HideReactions, HideStories import FeatPinnedChats import NGData @@ -400,6 +404,12 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { // MARK: Nicegram PinnedChats let nicegramItem: PinnedChatToDisplay? // + + // MARK: Nicegram ATT + let attBannerFeature = AttBannerFeature() + var attAd: AttAd? { nicegramItem?.chat.attAd } + // + let presentationData: ChatListPresentationData let context: AccountContext let chatListLocation: ChatListControllerLocation @@ -508,8 +518,12 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { if let nicegramItem { if #available(iOS 13.0, *) { Task { @MainActor in - let openPinnedChatUseCase = PinnedChatsContainer.shared.openPinnedChatUseCase() - openPinnedChatUseCase(nicegramItem.chat) + if let attAd { + attBannerFeature.onClick(ad: attAd) + } else { + let openPinnedChatUseCase = PinnedChatsContainer.shared.openPinnedChatUseCase() + openPinnedChatUseCase(nicegramItem.chat) + } interaction.clearHighlightAnimated(true) } @@ -601,7 +615,7 @@ private enum RevealOptionKey: Int32 { } private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) -> Bool { - if id.namespace == Namespaces.Peer.CloudUser && id.id._internalGetInt64Value() == 777000 { + if id.isTelegramNotifications { return false } if id == accountPeerId { @@ -938,7 +952,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } -private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) +private let loginCodeRegex = try? NSRegularExpression(pattern: "\\b\\d{5,8}\\b", options: []) public class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { @@ -1210,6 +1224,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var item: ChatListItem? + // MARK: Nicegram ATT + private var attClaimAnimationView: AttClaimAnimationView? + private var attClaimAnimationCancellable: AnyCancellable? + // + private let backgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -1464,10 +1483,30 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ) } self.authorNode.visibilityStatus = self.visibilityStatus + + // MARK: Nicegram ATT + trackViewIfNeeded() + // } } } + // MARK: Nicegram ATT + public func trackViewIfNeeded() { + guard self.visibilityStatus, + let item, + item.interaction.isChatListVisible() else { + return + } + + if let attAd = item.attAd { + item.attBannerFeature.onView(ad: attAd) + } else if let nicegramItem = item.nicegramItem { + PinnedChatsUI.trackChatView(nicegramItem.chat) + } + } + // + private var trackingIsInHierarchy: Bool = false { didSet { if self.trackingIsInHierarchy != oldValue { @@ -1825,6 +1864,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } // + // MARK: Nicegram ATT + attClaimAnimationCancellable = item.attBannerFeature.claimAnimationPublisher + .sink { [weak self] result in + self?.attClaimAnimationView?.animate(value: result.claimAmount) + } + // + self.contextContainer.isGestureEnabled = enablePreview && !item.editing } @@ -2341,14 +2387,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let messagePeer = itemPeer.chatMainPeer { peerText = messagePeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } - } else if let message = messages.last, let author = message.author?._asPeer(), let peer = itemPeer.chatMainPeer, !isUser { - if case let .channel(peer) = peer, case .broadcast = peer.info { - } else if !displayAsMessage { - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { - peerText = authorSignature - } else { - peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorIsCurrentChat = author.id == peer.id + } else if let message = messages.last, let author = message.author?._asPeer(), let peer = itemPeer.chatMainPeer { + if peer.id.isVerificationCodes { + if let message = messages.last, let forwardInfo = message.forwardInfo, let author = forwardInfo.author { + peerText = EnginePeer(author).compactDisplayTitle + } + } else if !isUser { + if case let .channel(peer) = peer, case .broadcast = peer.info { + } else if !displayAsMessage { + if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { + peerText = authorSignature + } else { + peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + authorIsCurrentChat = author.id == peer.id + } } } } @@ -2442,7 +2494,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { + if message.id.peerId.isTelegramNotifications || message.id.peerId.isVerificationCodes { if let cached = currentCustomTextEntities, cached.matches(text: message.text) { customTextEntities = cached } else if let matches = loginCodeRegex?.matches(in: message.text, options: [], range: NSMakeRange(0, (message.text as NSString).length)) { @@ -2560,7 +2612,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound) - if firstRangeOrigin > 24 { + if firstRangeOrigin > 24 && !chatListSearchResult.searchQuery.hasPrefix("#") { var leftOrigin: Int = 0 (composedString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in let distanceFromEnd = firstRangeOrigin - range1.location @@ -2619,7 +2671,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if !ignoreForwardedIcon { if case .savedMessagesChats = item.chatListLocation { displayForwardedIcon = false - } else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) { + } else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) && !message.id.peerId.isVerificationCodes { displayForwardedIcon = true } else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) { displayStoryReplyIcon = true @@ -2655,7 +2707,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case let .preview(dimensions, immediateThumbnailData, videoDuration): if let immediateThumbnailData { if let videoDuration { - let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil, coverTime: nil)]) + let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) contentImageSpecs.append(ContentImageSpec(message: message, media: .file(thumbnailMedia), size: fitSize)) } else { let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: index), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) @@ -4473,6 +4525,26 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mutedIconNode.isHidden = true } + // MARK: Nicegram ATT + if item.attAd != nil { + let attClaimAnimationView: AttClaimAnimationView + if let current = strongSelf.attClaimAnimationView { + attClaimAnimationView = current + } else { + attClaimAnimationView = AttClaimAnimationView() + strongSelf.attClaimAnimationView = attClaimAnimationView + strongSelf.mainContentContainerNode.view.addSubview(attClaimAnimationView) + } + + let iconSize = CGSize(width: 20, height: 20) + transition.updateFrame(view: attClaimAnimationView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize)) + nextTitleIconOrigin += attClaimAnimationView.bounds.width + 4.0 + } else if let attClaimAnimationView = strongSelf.attClaimAnimationView { + strongSelf.attClaimAnimationView = nil + attClaimAnimationView.removeFromSuperview() + } + // + let separatorInset: CGFloat if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault { separatorInset = 0.0 @@ -4660,10 +4732,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case RevealOptionKey.unpin.rawValue: self.setRevealOptionsOpened(false, animated: true) - Task { @MainActor in - PinnedChatsUI.unpin( - nicegramItem.chat - ) + if let ad = item.attAd { + item.attBannerFeature.onRemoveAdClick(ad: ad) + } else { + Task { @MainActor in + PinnedChatsUI.unpin( + nicegramItem.chat + ) + } } default: break diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 4dc4de1c29a..0882e448cde 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -246,7 +246,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: processed = true break inner } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { messageText = strings.Message_VideoMessage processed = true diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index e182680bc0f..d9651f9a6a8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -86,6 +86,7 @@ public final class ChatListNodeInteraction { // MARK: Nicegram PinnedChats let clearHighlightAnimated: (Bool) -> Void + let isChatListVisible: () -> Bool // let peerSelected: (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void @@ -116,7 +117,7 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void - let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void + let openPremiumGift: ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void let openPremiumManagement: () -> Void let openActiveSessions: () -> Void let openBirthdaySetup: () -> Void @@ -146,6 +147,7 @@ public final class ChatListNodeInteraction { activateSearch: @escaping () -> Void, // MARK: Nicegram PinnedChats clearHighlightAnimated: @escaping (Bool) -> Void = { _ in }, + isChatListVisible: @escaping () -> Bool = { false }, // peerSelected: @escaping (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, @@ -175,7 +177,7 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, - openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void, + openPremiumGift: @escaping ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void, openPremiumManagement: @escaping () -> Void, openActiveSessions: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void, @@ -190,6 +192,7 @@ public final class ChatListNodeInteraction { self.activateSearch = activateSearch // MARK: Nicegram PinnedChats self.clearHighlightAnimated = clearHighlightAnimated + self.isChatListVisible = isChatListVisible // self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected @@ -765,13 +768,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break case let .starsSubscriptionLowBalance(amount, _): @@ -1108,13 +1111,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break case let .starsSubscriptionLowBalance(amount, _): @@ -1276,6 +1279,12 @@ public final class ChatListNode: ListView { private var dequeuedInitialTransitionOnLayout = false private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? + // MARK: Nicegram PinnedChats + private let getPinnedChatsToDisplayUseCase = PinnedChatsContainer.shared.getPinnedChatsToDisplayUseCase() + public let isChatListVisible = CurrentValueSubject(false) + private var cancellables = Set() + // + public private(set) var currentState: ChatListNodeState private let statePromise: ValuePromise public var state: Signal { @@ -1421,6 +1430,8 @@ public final class ChatListNode: ListView { // MARK: Nicegram PinnedChats }, clearHighlightAnimated: { [weak self] flag in self?.clearHighlightAnimated(flag) + }, isChatListVisible: { [weak self] in + self?.isChatListVisible.value ?? false // }, peerSelected: { [weak self] peer, _, threadId, promoInfo in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { @@ -1745,13 +1756,24 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) - }, openPremiumGift: { [weak self] birthdays in + }, openPremiumGift: { [weak self] peers, birthdays in guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) - controller.navigationPresentation = .modal - self.push?(controller) + if peers.count == 1, let peerId = peers.first?.id { + let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions) + controller.navigationPresentation = .modal + self.push?(controller) + }) + } else { + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.push?(controller) + } }, openPremiumManagement: { [weak self] in guard let self else { return @@ -2254,16 +2276,27 @@ public final class ChatListNode: ListView { } // MARK: Nicegram PinnedChats - let nicegramItemsSignal: Signal<[PinnedChatToDisplay], NoError> - if #available(iOS 13.0, *) { - nicegramItemsSignal = PinnedChatsContainer.shared.getPinnedChatsToDisplayUseCase() - .publisher() - .map { $0.reversed() } - .toSignal() - .skipError() - } else { - nicegramItemsSignal = .single([]) - } + let nicegramItemsSignal = getPinnedChatsToDisplayUseCase + .publisher( + isViewVisible: isChatListVisible.eraseToAnyPublisher() + ) + .map { Array($0.reversed()) } + .toSignal() + .skipError() + + isChatListVisible + .removeDuplicates() + .sink { [weak self] isChatListVisible in + guard let self else { return } + if isChatListVisible { + self.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatListItemNode { + itemNode.trackViewIfNeeded() + } + } + } + } + .store(in: &cancellables) // // MARK: Nicegram HiddenChats @@ -2354,10 +2387,6 @@ public final class ChatListNode: ListView { if case .forum(_) = location { nicegramItems = [] } - - if #available(iOS 13.0, *) { - PinnedChatsUI.trackChatsView(nicegramItems.map(\.chat)) - } // let innerIsMainTab = location == .chatList(groupId: .root) && chatListFilter == nil @@ -2388,7 +2417,7 @@ public final class ChatListNode: ListView { guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false } if let peer = peer.peer { - if peer.id.isReplies { + if peer.id.isRepliesOrVerificationCodes { return false } @@ -4339,7 +4368,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres } } - if peer.id.isReplies { + if peer.id.isReplies || peer.id.isVerificationCodes { return nil } else if case let .user(user) = peer { if user.botInfo != nil || user.flags.contains(.isSupport) { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index bd28cebcc21..b057278376e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -194,6 +194,11 @@ enum ChatListNodeEntry: Comparable, Identifiable { } static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool { + // MARK: Nicegram PinnedChats + if lhs.nicegramItem != rhs.nicegramItem { + return false + } + // if lhs.index != rhs.index { return false } @@ -671,6 +676,10 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, pinnedIndexOffset += UInt16(filteredAdditionalItemEntries.count) } + // MARK: Nicegram PinnedChats + pinnedIndexOffset += UInt16(nicegramItems.count) + // + var hiddenGeneralThread: ChatListNodeEntry? loop: for entry in view.items { @@ -773,10 +782,6 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, if !view.hasLater { var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1)) - // MARK: Nicegram PinnedChats - pinningIndex += UInt16(nicegramItems.count) - // - if let savedMessagesPeer = savedMessagesPeer { if !foundPeers.isEmpty { var foundPinningIndex: UInt16 = UInt16(foundPeers.count) diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 0152458b2ce..6536a80b340 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -500,7 +500,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let activeGroupCallInfo: ChatActiveGroupCallInfo? public let hasActiveGroupCall: Bool public let importState: ChatPresentationImportState? - public let reportReason: ReportReason? + public let reportReason: (String, Data, String?)? public let showCommands: Bool public let hasBotCommands: Bool public let showSendAsPeers: Bool @@ -509,6 +509,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let showWebView: Bool public let currentSendAsPeerId: PeerId? public let copyProtectionEnabled: Bool + public let hasAtLeast3Messages: Bool public let hasPlentyOfMessages: Bool public let isPremium: Bool public let premiumGiftOptions: [CachedPremiumGiftOption] @@ -530,6 +531,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let boostsToUnrestrict: Int32? public let businessIntro: TelegramBusinessIntro? public let hasBirthdayToday: Bool + public let adMessage: Message? public init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, threadData: ThreadData?, isGeneralThreadClosed: Bool?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, businessIntro: TelegramBusinessIntro?) { self.interfaceState = ChatInterfaceState() @@ -592,6 +594,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.showWebView = false self.currentSendAsPeerId = nil self.copyProtectionEnabled = false + self.hasAtLeast3Messages = false self.hasPlentyOfMessages = false self.isPremium = false self.premiumGiftOptions = [] @@ -613,9 +616,10 @@ public final class ChatPresentationInterfaceState: Equatable { self.boostsToUnrestrict = nil self.businessIntro = businessIntro self.hasBirthdayToday = false + self.adMessage = nil } - public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, historyFilter: HistoryFilter?, displayHistoryFilterAsList: Bool, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, savedMessagesTopicPeer: EnginePeer?, hasSearchTags: Bool, isPremiumRequiredForMessaging: Bool, hasSavedChats: Bool, appliedBoosts: Int32?, boostsToUnrestrict: Int32?, businessIntro: TelegramBusinessIntro?, hasBirthdayToday: Bool) { + public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, historyFilter: HistoryFilter?, displayHistoryFilterAsList: Bool, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: (String, Data, String?)?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasAtLeast3Messages: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, savedMessagesTopicPeer: EnginePeer?, hasSearchTags: Bool, isPremiumRequiredForMessaging: Bool, hasSavedChats: Bool, appliedBoosts: Int32?, boostsToUnrestrict: Int32?, businessIntro: TelegramBusinessIntro?, hasBirthdayToday: Bool, adMessage: Message?) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -676,6 +680,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.showWebView = showWebView self.currentSendAsPeerId = currentSendAsPeerId self.copyProtectionEnabled = copyProtectionEnabled + self.hasAtLeast3Messages = hasAtLeast3Messages self.hasPlentyOfMessages = hasPlentyOfMessages self.isPremium = isPremium self.premiumGiftOptions = premiumGiftOptions @@ -697,6 +702,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.boostsToUnrestrict = boostsToUnrestrict self.businessIntro = businessIntro self.hasBirthdayToday = hasBirthdayToday + self.adMessage = adMessage } public static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -859,7 +865,7 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.importState != rhs.importState { return false } - if lhs.reportReason != rhs.reportReason { + if lhs.reportReason?.0 != rhs.reportReason?.0 || lhs.reportReason?.1 != rhs.reportReason?.1 || lhs.reportReason?.2 != rhs.reportReason?.2 { return false } if lhs.showCommands != rhs.showCommands { @@ -886,6 +892,9 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.copyProtectionEnabled != rhs.copyProtectionEnabled { return false } + if lhs.hasAtLeast3Messages != rhs.hasAtLeast3Messages { + return false + } if lhs.hasPlentyOfMessages != rhs.hasPlentyOfMessages { return false } @@ -949,35 +958,38 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.hasBirthdayToday != rhs.hasBirthdayToday { return false } + if lhs.adMessage?.id != rhs.adMessage?.id { + return false + } return true } public func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsNotAccessible(_ isNotAccessible: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedExplicitelyCanPinMessages(_ explicitelyCanPinMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedContactStatus(_ contactStatus: ChatContactStatus?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBots(_ hasBots: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsArchived(_ isArchived: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -989,271 +1001,279 @@ public final class ChatPresentationInterfaceState: Equatable { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerDiscussionId(_ peerDiscussionId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCallsAvailable(_ callsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCallsPrivate(_ callsPrivate: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSlowmodeState(_ slowmodeState: ChatSlowmodeState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHistoryFilter(_ historyFilter: HistoryFilter?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedDisplayHistoryFilterAsList(_ displayHistoryFilterAsList: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPresentationReady(_ presentationReady: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTheme(_ theme: PresentationTheme) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedStrings(_ strings: PresentationStrings) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedDateTimeFormat(_ dateTimeFormat: PresentationDateTimeFormat) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedChatWallpaper(_ chatWallpaper: TelegramWallpaper) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBubbleCorners(_ bubbleCorners: PresentationChatBubbleCorners) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSubject(_ subject: ChatControllerSubject?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPendingUnpinnedAllMessages(_ pendingUnpinnedAllMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedActiveGroupCallInfo(_ activeGroupCallInfo: ChatActiveGroupCallInfo?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasActiveGroupCall(_ hasActiveGroupCall: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedImportState(_ importState: ChatPresentationImportState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } - public func updatedReportReason(_ reportReason: ReportReason?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + public func updatedReportReason(_ reportReason: (String, Data, String?)?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowCommands(_ showCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBotCommands(_ hasBotCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowSendAsPeers(_ showSendAsPeers: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSendAsPeers(_ sendAsPeers: [SendAsPeer]?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCurrentSendAsPeerId(_ currentSendAsPeerId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBotMenuButton(_ botMenuButton: BotMenuButton) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowWebView(_ showWebView: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCopyProtectionEnabled(_ copyProtectionEnabled: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) + } + + public func updatedHasAtLeast3Messages(_ hasAtLeast3Messages: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasPlentyOfMessages(_ hasPlentyOfMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsPremium(_ isPremium: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSuggestPremiumGift(_ suggestPremiumGift: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedForceInputCommandsHidden(_ forceInputCommandsHidden: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCustomEmojiAvailable(_ customEmojiAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedThreadData(_ threadData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedForumTopicData(_ forumTopicData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsGeneralThreadClosed(_ isGeneralThreadClosed: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTranslationState(_ translationState: ChatPresentationTranslationState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedReplyMessage(_ replyMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAccountPeerColor(_ accountPeerColor: AccountPeerColor?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSavedMessagesTopicPeer(_ savedMessagesTopicPeer: EnginePeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasSearchTags(_ hasSearchTags: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsPremiumRequiredForMessaging(_ isPremiumRequiredForMessaging: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasSavedChats(_ hasSavedChats: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAppliedBoosts(_ appliedBoosts: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBoostsToUnrestrict(_ boostsToUnrestrict: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBirthdayToday(_ hasBirthdayToday: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday, adMessage: self.adMessage) + } + + public func updatedAdMessage(_ adMessage: Message?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: adMessage) } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index b42f21063e2..53dcb81b794 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -748,11 +748,15 @@ public struct ComponentTransition { } public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateScale(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion) + } + + public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) case let .curve(duration, curve): - view.layer.animate( + layer.animate( from: fromValue as NSNumber, to: toValue as NSNumber, keyPath: "transform.scale", diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 1ea0212393f..7a3caa1c4ed 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -13,17 +13,19 @@ public final class RoundedRectangle: Component { public let gradientDirection: GradientDirection public let stroke: CGFloat? public let strokeColor: UIColor? + public let size: CGSize? - public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { - self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor) + public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) { + self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor, size: size) } - public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection self.stroke = stroke self.strokeColor = strokeColor + self.size = size } public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool { @@ -42,6 +44,9 @@ public final class RoundedRectangle: Component { if lhs.strokeColor != rhs.strokeColor { return false } + if lhs.size != rhs.size { + return false + } return true } @@ -49,8 +54,9 @@ public final class RoundedRectangle: Component { var component: RoundedRectangle? func update(component: RoundedRectangle, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + let size = component.size ?? availableSize if self.component != component { - let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5 + let cornerRadius = component.cornerRadius ?? min(size.width, size.height) * 0.5 if component.colors.count == 1, let color = component.colors.first { let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0) @@ -75,7 +81,7 @@ public final class RoundedRectangle: Component { self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius)) UIGraphicsEndImageContext() } else if component.colors.count > 1 { - let imageSize = availableSize + let imageSize = size UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath) @@ -106,7 +112,7 @@ public final class RoundedRectangle: Component { } } - return availableSize + return size } } @@ -120,13 +126,18 @@ public final class RoundedRectangle: Component { } public final class FilledRoundedRectangleComponent: Component { + public enum CornerRadius: Equatable { + case value(CGFloat) + case minEdge + } + public let color: UIColor - public let cornerRadius: CGFloat + public let cornerRadius: CornerRadius public let smoothCorners: Bool public init( color: UIColor, - cornerRadius: CGFloat, + cornerRadius: CornerRadius, smoothCorners: Bool ) { self.color = color @@ -210,9 +221,17 @@ public final class FilledRoundedRectangleComponent: Component { transition.setTintColor(view: self, color: component.color) - if self.currentCornerRadius != component.cornerRadius { + let cornerRadius: CGFloat + switch component.cornerRadius { + case let .value(value): + cornerRadius = value + case .minEdge: + cornerRadius = min(availableSize.width, availableSize.height) * 0.5 + } + + if self.currentCornerRadius != cornerRadius { let previousCornerRadius = self.currentCornerRadius - self.currentCornerRadius = component.cornerRadius + self.currentCornerRadius = cornerRadius if transition.animation.isImmediate { self.applyStaticCornerRadius() } else { @@ -230,7 +249,7 @@ public final class FilledRoundedRectangleComponent: Component { } } - transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius, completion: { [weak self] completed in + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return } diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift index 0d7dd997693..30453c21226 100644 --- a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -23,6 +23,7 @@ public final class BalancedTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -41,6 +42,7 @@ public final class BalancedTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -58,6 +60,7 @@ public final class BalancedTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -122,6 +125,10 @@ public final class BalancedTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -165,6 +172,7 @@ public final class BalancedTextComponent: Component { self.textView.textShadowBlur = component.textShadowBlur self.textView.textStroke = component.textStroke self.textView.linkHighlightColor = component.highlightColor + self.textView.linkHighlightInset = component.highlightInset self.textView.highlightAttributeAction = component.highlightAction self.textView.tapAttributeAction = component.tapAction self.textView.longTapAttributeAction = component.longTapAction diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index d6165246afb..b561f12bee3 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -8,11 +8,15 @@ public final class BundleIconComponent: Component { public let name: String public let tintColor: UIColor? public let maxSize: CGSize? + public let shadowColor: UIColor? + public let shadowBlur: CGFloat - public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) { self.name = name self.tintColor = tintColor self.maxSize = maxSize + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool { @@ -25,6 +29,12 @@ public final class BundleIconComponent: Component { if lhs.maxSize != rhs.maxSize { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } @@ -40,12 +50,24 @@ public final class BundleIconComponent: Component { } func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - if self.component?.name != component.name || self.component?.tintColor != component.tintColor { + if self.component?.name != component.name || self.component?.tintColor != component.tintColor || self.component?.shadowColor != component.shadowColor || self.component?.shadowBlur != component.shadowBlur { + var image: UIImage? if let tintColor = component.tintColor { - self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) + image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) } else { - self.image = UIImage(bundleImageName: component.name) + image = UIImage(bundleImageName: component.name) + } + if let imageValue = image, let shadowColor = component.shadowColor, component.shadowBlur != 0.0 { + image = generateImage(CGSize(width: imageValue.size.width + component.shadowBlur * 2.0, height: imageValue.size.height + component.shadowBlur * 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(), blur: component.shadowBlur, color: shadowColor.cgColor) + + if let cgImage = imageValue.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: component.shadowBlur, y: component.shadowBlur), size: imageValue.size)) + } + }) } + self.image = image } self.component = component diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 4a1f5886567..cfcbaf9b855 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -22,6 +22,7 @@ public final class MultilineTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -39,6 +40,7 @@ public final class MultilineTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -55,6 +57,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -116,6 +119,10 @@ public final class MultilineTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -143,6 +150,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = component.textShadowBlur self.textStroke = component.textStroke self.linkHighlightColor = component.highlightColor + self.linkHighlightInset = component.highlightInset self.highlightAttributeAction = component.highlightAction self.tapAttributeAction = component.tapAction self.longTapAttributeAction = component.longTapAction diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index a0f9198def5..e86e95faad8 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textShadowColor: UIColor? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let handleSpoilers: Bool public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -50,6 +51,7 @@ public final class MultilineTextWithEntitiesComponent: Component { textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + handleSpoilers: Bool = false, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -70,6 +72,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textStroke = textStroke self.highlightColor = highlightColor self.highlightAction = highlightAction + self.handleSpoilers = handleSpoilers self.tapAction = tapAction self.longTapAction = longTapAction } @@ -99,7 +102,9 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.insets != rhs.insets { return false } - + if lhs.handleSpoilers != rhs.handleSpoilers { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -131,6 +136,7 @@ public final class MultilineTextWithEntitiesComponent: Component { } public final class View: UIView { + var spoilerTextNode: ImmediateTextNodeWithEntities? let textNode: ImmediateTextNodeWithEntities public override init(frame: CGRect) { @@ -170,7 +176,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.highlightAttributeAction = component.highlightAction self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction - + if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string { if let snapshotView = self.snapshotContentTree() { snapshotView.center = self.center @@ -197,6 +203,45 @@ public final class MultilineTextWithEntitiesComponent: Component { let size = self.textNode.updateLayout(availableSize) self.textNode.frame = CGRect(origin: .zero, size: size) + if component.handleSpoilers { + let spoilerTextNode: ImmediateTextNodeWithEntities + if let current = self.spoilerTextNode { + spoilerTextNode = current + } else { + spoilerTextNode = ImmediateTextNodeWithEntities() + spoilerTextNode.alpha = 0.0 + self.spoilerTextNode = spoilerTextNode + + self.textNode.dustNode?.textNode = spoilerTextNode + } + + spoilerTextNode.displaySpoilers = true + spoilerTextNode.displaySpoilerEffect = false + spoilerTextNode.attributedText = attributedString + spoilerTextNode.maximumNumberOfLines = component.maximumNumberOfLines + spoilerTextNode.truncationType = component.truncationType + spoilerTextNode.textAlignment = component.horizontalAlignment + spoilerTextNode.verticalAlignment = component.verticalAlignment + spoilerTextNode.lineSpacing = component.lineSpacing + spoilerTextNode.cutout = component.cutout + spoilerTextNode.insets = component.insets + spoilerTextNode.textShadowColor = component.textShadowColor + spoilerTextNode.textStroke = component.textStroke + spoilerTextNode.isUserInteractionEnabled = false + + let size = spoilerTextNode.updateLayout(availableSize) + spoilerTextNode.frame = CGRect(origin: .zero, size: size) + + if spoilerTextNode.view.superview == nil { + self.addSubview(spoilerTextNode.view) + } + } else if let spoilerTextNode = self.spoilerTextNode { + self.spoilerTextNode = nil + spoilerTextNode.view.removeFromSuperview() + + self.textNode.dustNode?.textNode = nil + } + return size } } diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 7ad116ee033..9b450ce4ac8 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -367,20 +367,24 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { struct Counter: Equatable { var components: [CounterLayout.Component] + var extractedComponents: [CounterLayout.Component] } struct Layout: Equatable { var colors: Colors var size: CGSize + var extractedSize: CGSize var counter: Counter? var isTag: Bool } private struct AnimationState { var fromCounter: Counter? + var fromExtracted: Bool var fromColors: Colors var startTime: Double var duration: Double + var curve: ComponentTransition.Animation.Curve } private var isExtracted: Bool = false @@ -391,6 +395,9 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { override init(frame: CGRect) { super.init(frame: CGRect()) + + self.layer.contentsScale = UIScreenScale + self.layer.contentsGravity = .topLeft } required init?(coder: NSCoder) { @@ -405,13 +412,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { func update(layout: Layout) { if self.currentLayout != layout { if let currentLayout = self.currentLayout, (currentLayout.counter != layout.counter || currentLayout.colors.isSelected != layout.colors.isSelected) { - self.animationState = AnimationState(fromCounter: currentLayout.counter, fromColors: currentLayout.colors, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor()) + self.animationState = AnimationState(fromCounter: currentLayout.counter, fromExtracted: self.isExtracted, fromColors: currentLayout.colors, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor(), curve: .linear) } self.currentLayout = layout self.updateBackgroundImage(animated: false) - self.updateAnimation() } } @@ -447,7 +453,21 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { func updateIsExtracted(isExtracted: Bool, animated: Bool) { if self.isExtracted != isExtracted { self.isExtracted = isExtracted - self.updateBackgroundImage(animated: animated) + + if let currentLayout = self.currentLayout { + self.animationState = AnimationState( + fromCounter: currentLayout.counter, + fromExtracted: !isExtracted, + fromColors: currentLayout.colors, + startTime: CACurrentMediaTime(), + duration: 0.5 * UIView.animationDurationFactor(), + curve: .spring + ) + self.updateBackgroundImage(animated: false) + updateAnimation() + } else { + self.updateBackgroundImage(animated: true) + } } } @@ -456,9 +476,18 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { return } - var totalComponentWidth: CGFloat = 0.0 + var counterComponents: [CounterLayout.Component]? if let counter = layout.counter { - for component in counter.components { + if self.isExtracted { + counterComponents = counter.extractedComponents + } else { + counterComponents = counter.components + } + } + + var totalComponentWidth: CGFloat = 0.0 + if let counterComponents { + for component in counterComponents { totalComponentWidth += component.bounds.width } } @@ -466,8 +495,30 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let isExtracted = self.isExtracted let animationState = self.animationState + var animationFraction: CGFloat + var fixedTransitionDirection: Bool? + if let animationState, animationState.fromCounter != nil { + animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration)) + animationFraction = animationState.curve.solve(at: animationFraction) + if animationState.fromExtracted != isExtracted { + fixedTransitionDirection = isExtracted ? true : false + } + } else { + animationFraction = 1.0 + } + + let targetImageSize = isExtracted ? layout.extractedSize : layout.size + var imageSize = targetImageSize + if let animationState { + let sourceImageSize = animationState.fromExtracted ? layout.extractedSize : layout.size + imageSize = CGSize( + width: floor(sourceImageSize.width * (1.0 - animationFraction) + targetImageSize.width * animationFraction), + height: floor(sourceImageSize.height * (1.0 - animationFraction) + targetImageSize.height * animationFraction) + ) + } + DispatchQueue.global().async { [weak self] in - let image = generateImage(layout.size, rotatedContext: { size, context in + let image = generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) @@ -511,7 +562,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) } - if let counter = layout.counter { + if let counterComponents { let isForegroundTransparent = foregroundColor.alpha < 1.0 context.setBlendMode(isForegroundTransparent ? .copy : .normal) @@ -522,39 +573,53 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { textOrigin = 36.0 } - var rightTextOrigin = textOrigin + totalComponentWidth + var leftTextOrigin = textOrigin - let animationFraction: CGFloat - if let animationState = animationState, animationState.fromCounter != nil { - animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration)) - } else { - animationFraction = 1.0 - } - - for i in (0 ..< counter.components.count).reversed() { - let component = counter.components[i] + for i in 0 ..< counterComponents.count { + let component = counterComponents[i] var componentAlpha: CGFloat = 1.0 var componentVerticalOffset: CGFloat = 0.0 + var componentAnimationFraction = animationFraction + if let animationState = animationState, let fromCounter = animationState.fromCounter { - let reverseIndex = counter.components.count - 1 - i - if reverseIndex < fromCounter.components.count { - let previousComponent = fromCounter.components[fromCounter.components.count - 1 - reverseIndex] + let fromCounterComponents = animationState.fromExtracted ? fromCounter.extractedComponents : fromCounter.components + + let countNorm = max(counterComponents.count, fromCounterComponents.count) + let countFraction = CGFloat(i + 1) / CGFloat(countNorm) + + let minDurationCompression = 0.25 + let maxDurationCompression = 1.0 + + let durationCompression = minDurationCompression * (1.0 - countFraction) + maxDurationCompression * countFraction + + let adjustedDuration = animationState.duration * durationCompression + + componentAnimationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / adjustedDuration)) + componentAnimationFraction = animationState.curve.solve(at: componentAnimationFraction) + + if i < fromCounterComponents.count { + let previousComponent = fromCounterComponents[i] if previousComponent != component { - componentAlpha = animationFraction - componentVerticalOffset = -(1.0 - animationFraction) * 8.0 - if previousComponent.string < component.string { - componentVerticalOffset = -componentVerticalOffset - } - + componentAlpha = componentAnimationFraction + componentVerticalOffset = -(1.0 - componentAnimationFraction) * 12.0 let previousComponentAlpha = 1.0 - componentAlpha - var previousComponentVerticalOffset = animationFraction * 8.0 - if previousComponent.string < component.string { - previousComponentVerticalOffset = -previousComponentVerticalOffset + var previousComponentVerticalOffset = componentAnimationFraction * 12.0 + + if let fixedTransitionDirection { + if !fixedTransitionDirection { + componentVerticalOffset = -componentVerticalOffset + previousComponentVerticalOffset = -previousComponentVerticalOffset + } + } else { + if previousComponent.string < component.string { + componentVerticalOffset = -componentVerticalOffset + previousComponentVerticalOffset = -previousComponentVerticalOffset + } } - var componentOrigin = rightTextOrigin - previousComponent.bounds.width + var componentOrigin = leftTextOrigin componentOrigin = max(componentOrigin, layout.size.height / 2.0 + UIScreenPixel) let previousColor: UIColor if isForegroundTransparent { @@ -565,10 +630,18 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor) string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset)) } + } else { + componentAlpha = componentAnimationFraction + componentVerticalOffset = -(1.0 - componentAnimationFraction) * 12.0 + if let fixedTransitionDirection { + if !fixedTransitionDirection { + componentVerticalOffset = -componentVerticalOffset + } + } } } - let componentOrigin = rightTextOrigin - component.bounds.width + let componentOrigin = leftTextOrigin let currentColor: UIColor if isForegroundTransparent { currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha) @@ -578,7 +651,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor) string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset)) - rightTextOrigin -= component.bounds.width + leftTextOrigin += component.bounds.width } } } @@ -591,6 +664,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { if !layout.colors.isSelected { animationFraction = 1.0 - animationFraction } + animationFraction = animationState.curve.solve(at: animationFraction) let center = CGPoint(x: 21.0, y: size.height / 2.0) let diameter = 0.0 * (1.0 - animationFraction) + (size.width - center.x) * 2.0 * animationFraction @@ -619,7 +693,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { if let strongSelf = self, let image = image { let previousContents = strongSelf.layer.contents - ASDisplayNodeSetResizableContents(strongSelf.layer, image) + //ASDisplayNodeSetResizableContents(strongSelf.layer, image) + strongSelf.layer.contents = image.cgImage if animated, let previousContents = previousContents { strongSelf.layer.animate(from: previousContents as! CGImage, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) @@ -714,10 +789,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let imageSize: CGSize let counterLayout: CounterLayout? + let extractedCounterLayout: CounterLayout? let backgroundLayout: ContainerButtonNode.Layout let size: CGSize + let extractedSize: CGSize init( spec: Spec, @@ -726,8 +803,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { imageFrame: CGRect, imageSize: CGSize, counterLayout: CounterLayout?, + extractedCounterLayout: CounterLayout?, backgroundLayout: ContainerButtonNode.Layout, - size: CGSize + size: CGSize, + extractedSize: CGSize ) { self.spec = spec self.backgroundColor = backgroundColor @@ -735,8 +814,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { self.imageFrame = imageFrame self.imageSize = imageSize self.counterLayout = counterLayout + self.extractedCounterLayout = extractedCounterLayout self.backgroundLayout = backgroundLayout self.size = size + self.extractedSize = extractedSize } static func calculate(spec: Spec, currentLayout: Layout?) -> Layout { @@ -748,14 +829,25 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let imageSize: CGSize = boundingImageSize var counterComponents: [String] = [] + var extractedCounterComponents: [String] = [] var hasTitle = false if let title = spec.component.reaction.title, !title.isEmpty { hasTitle = true counterComponents.append(title) + extractedCounterComponents.append(title) } else { - for character in countString(Int64(spec.component.count)) { + #if DEBUG && false + let compactString = "4K" + #else + let compactString = countString(Int64(spec.component.count)) + #endif + + for character in compactString { counterComponents.append(String(character)) } + for character in "\(spec.component.count)" { + extractedCounterComponents.append(String(character)) + } } let backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground @@ -768,8 +860,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } var counterLayout: CounterLayout? + var extractedCounterLayout: CounterLayout? var size = CGSize(width: boundingImageSize.width + sideInsets * 2.0, height: height) + var extractedSize = size if !spec.component.avatarPeers.isEmpty { size.width += 4.0 + 24.0 if spec.component.avatarPeers.count > 1 { @@ -777,25 +871,48 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } else { size.width -= 2.0 } + extractedSize = size } else if spec.component.isTag && !hasTitle { size.width += 1.0 + extractedSize = size } else { - let counterSpec = CounterLayout.Spec( - stringComponents: counterComponents - ) - let counterValue: CounterLayout - if let currentCounter = currentLayout?.counterLayout, currentCounter.spec == counterSpec { - counterValue = currentCounter - } else { - counterValue = CounterLayout.calculate( - spec: counterSpec, - previousLayout: currentLayout?.counterLayout + do { + let counterSpec = CounterLayout.Spec( + stringComponents: counterComponents ) + let counterValue: CounterLayout + if let currentCounter = currentLayout?.counterLayout, currentCounter.spec == counterSpec { + counterValue = currentCounter + } else { + counterValue = CounterLayout.calculate( + spec: counterSpec, + previousLayout: currentLayout?.counterLayout + ) + } + counterLayout = counterValue + size.width += spacing + counterValue.size.width + if spec.component.isTag { + size.width += 5.0 + } } - counterLayout = counterValue - size.width += spacing + counterValue.size.width - if spec.component.isTag { - size.width += 5.0 + do { + let extractedCounterSpec = CounterLayout.Spec( + stringComponents: extractedCounterComponents + ) + let extractedCounterValue: CounterLayout + if let currentExtractedCounter = currentLayout?.extractedCounterLayout, currentExtractedCounter.spec == extractedCounterSpec { + extractedCounterValue = currentExtractedCounter + } else { + extractedCounterValue = CounterLayout.calculate( + spec: extractedCounterSpec, + previousLayout: currentLayout?.extractedCounterLayout + ) + } + extractedCounterLayout = extractedCounterValue + extractedSize.width += spacing + extractedCounterValue.size.width + if spec.component.isTag { + extractedSize.width += 5.0 + } } } @@ -821,14 +938,16 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { ) } var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter? - if let counterLayout = counterLayout { + if let counterLayout, let extractedCounterLayout { backgroundCounter = ReactionButtonAsyncNode.ContainerButtonNode.Counter( - components: counterLayout.components + components: counterLayout.components, + extractedComponents: extractedCounterLayout.components ) } let backgroundLayout = ContainerButtonNode.Layout( colors: backgroundColors, size: size, + extractedSize: extractedSize, counter: backgroundCounter, isTag: spec.component.isTag ) @@ -840,8 +959,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { imageFrame: imageFrame, imageSize: boundingImageSize, counterLayout: counterLayout, + extractedCounterLayout: extractedCounterLayout, backgroundLayout: backgroundLayout, - size: size + size: size, + extractedSize: extractedSize ) } } @@ -1056,9 +1177,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) { - self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size) - self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.size) - self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size) + self.containerView.frame = CGRect(origin: CGPoint(), size: layout.extractedSize) + self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.extractedSize) + self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.extractedSize) + let buttonFrame = CGRect(origin: CGPoint(), size: layout.size) animation.animator.updatePosition(layer: self.buttonNode.layer, position: buttonFrame.center, completion: nil) animation.animator.updateBounds(layer: self.buttonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil) diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 39124655941..7e61cbe1d5b 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -366,6 +366,7 @@ public final class SheetComponent: Component { self.scrollView.addSubview(contentView) } contentView.clipsToBounds = component.clipsContent + contentView.layer.cornerRadius = self.backgroundView.layer.cornerRadius if sheetEnvironment.isCentered { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 966d2d5a2e5..d15bf6e0065 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -103,6 +103,16 @@ public struct ContactsPeerItemAction { } } +public struct ContactsPeerItemButtonAction { + public let title: String + public let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? + + public init(title: String, action: @escaping (ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void) { + self.title = title + self.action = action + } +} + public enum ContactsPeerItemPeer: Equatable { case thread(peer: EnginePeer, title: String, icon: Int64?, color: Int32) case peer(peer: EnginePeer?, chatPeer: EnginePeer?) @@ -175,6 +185,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] let actionIcon: ContactsPeerItemActionIcon + let buttonAction: ContactsPeerItemButtonAction? + let searchQuery: String? + let alwaysShowLastSeparator: Bool let action: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? @@ -213,8 +226,11 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, + buttonAction: ContactsPeerItemButtonAction? = nil, index: SortIndex?, header: ListViewItemHeader?, + searchQuery: String? = nil, + alwaysShowLastSeparator: Bool = false, action: ((ContactsPeerItemPeer) -> Void)?, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, @@ -245,6 +261,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.options = options self.additionalActions = additionalActions self.actionIcon = actionIcon + self.buttonAction = buttonAction + self.searchQuery = searchQuery + self.alwaysShowLastSeparator = alwaysShowLastSeparator self.action = action self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -427,6 +446,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var arrowButtonNode: HighlightableButtonNode? private var rightLabelTextNode: TextNode? + private var actionButtonNode: HighlightTrackingButtonNode? + private var actionButtonTitleNode: TextNode? + private var actionButtonBackgroundNode: ASImageNode? + private var avatarTapRecognizer: UITapGestureRecognizer? private var isHighlighted: Bool = false @@ -690,6 +713,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let makeRightLabelTextLayout = TextNode.asyncLayout(self.rightLabelTextNode) + let makeActionButtonTitleLayuout = TextNode.asyncLayout(self.actionButtonTitleNode) + let currentItem = self.layoutParams?.0 return { [weak self] item, params, first, last, firstWithHeader, neighbors in @@ -880,7 +905,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) } case let .addressName(suffix): - if let addressName = peer.addressName { + var addressName = peer.addressName + if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) { + for username in peer.usernames { + if username.username.lowercased().contains(searchQuery) { + addressName = username.username + break + } + } + } + if let addressName { let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor) if !suffix.isEmpty { let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) @@ -1015,6 +1049,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusHeightComponent = -1.0 + statusLayout.size.height } + var actionButtonTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let buttonAction = item.buttonAction { + actionButtonTitleLayoutAndApply = makeActionButtonTitleLayuout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonAction.title, font: Font.semibold(15.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + } + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + statusHeightComponent), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let titleFrame: CGRect @@ -1399,6 +1438,73 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { verifiedIconView.removeFromSuperview() } + var additionalRightInset: CGFloat = 0.0 + if let (titleLayout, titleApply) = actionButtonTitleLayoutAndApply { + let actionButtonTitleNode = titleApply() + let actionButtonBackgroundNode: ASImageNode + let actionButtonNode: HighlightTrackingButtonNode + if let currentBackgroundNode = strongSelf.actionButtonBackgroundNode, let currentButtonNode = strongSelf.actionButtonNode { + actionButtonBackgroundNode = currentBackgroundNode + actionButtonNode = currentButtonNode + } else { + actionButtonBackgroundNode = ASImageNode() + actionButtonBackgroundNode.displaysAsynchronously = false + strongSelf.offsetContainerNode.addSubnode(actionButtonBackgroundNode) + strongSelf.actionButtonBackgroundNode = actionButtonBackgroundNode + + actionButtonNode = HighlightTrackingButtonNode() + actionButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.actionButtonTitleNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.actionButtonTitleNode?.alpha = 0.4 + strongSelf.actionButtonBackgroundNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.actionButtonBackgroundNode?.alpha = 0.4 + } else { + strongSelf.actionButtonTitleNode?.alpha = 1.0 + strongSelf.actionButtonTitleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.actionButtonBackgroundNode?.alpha = 1.0 + strongSelf.actionButtonBackgroundNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed(_:)), forControlEvents: .touchUpInside) + + strongSelf.offsetContainerNode.addSubnode(actionButtonNode) + strongSelf.actionButtonNode = actionButtonNode + } + if strongSelf.actionButtonTitleNode == nil { + strongSelf.actionButtonTitleNode = actionButtonTitleNode + strongSelf.offsetContainerNode.insertSubnode(actionButtonTitleNode, aboveSubnode: actionButtonBackgroundNode) + } + if updatedTheme != nil || actionButtonBackgroundNode.image == nil { + actionButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemAccentColor) + } + + let actionButtonSize = CGSize(width: titleLayout.size.width + 13.0 * 2.0, height: 28.0) + let actionButtonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 12.0 - actionButtonSize.width, y: floorToScreenPixels((nodeLayout.contentSize.height - actionButtonSize.height) / 2.0)), size: actionButtonSize) + actionButtonBackgroundNode.frame = actionButtonFrame + actionButtonNode.frame = actionButtonFrame + + let actionTitleFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX + 13.0, y: actionButtonFrame.minY + floorToScreenPixels((actionButtonFrame.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size) + actionButtonTitleNode.frame = actionTitleFrame + + additionalRightInset += actionButtonSize.width + 16.0 + } else { + if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { + strongSelf.actionButtonTitleNode = nil + actionButtonTitleNode.removeFromSupernode() + } + if let actionButtonBackgroundNode = strongSelf.actionButtonBackgroundNode { + strongSelf.actionButtonBackgroundNode = nil + actionButtonBackgroundNode.removeFromSupernode() + } + if let actionButtonNode = strongSelf.actionButtonNode { + strongSelf.actionButtonNode = nil + actionButtonNode.removeFromSupernode() + } + } + if let actionButtons, actionButtons.count == 1, let actionButton = actionButtons.first, case .more = actionButton.type { let moreButtonNode: MoreButtonNode if let current = strongSelf.moreButtonNode { @@ -1414,7 +1520,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { actionButton.action?(item.peer, sourceNode, gesture) } let moreButtonSize = moreButtonNode.measure(CGSize(width: 100.0, height: nodeLayout.contentSize.height)) - moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 21.0 - moreButtonSize.width, y:floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) + moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 21.0 - moreButtonSize.width, y: floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) } else if let actionButtons = actionButtons { if strongSelf.actionButtonNodes == nil { var actionButtonNodes: [HighlightableButtonNode] = [] @@ -1485,7 +1591,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { badgeBackgroundNode.image = currentBadgeBackgroundImage badgeBackgroundWidth = max(badgeTextLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) - var badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) + var badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - additionalRightInset - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) if let arrowButtonImage = arrowButtonImage { badgeBackgroundFrame.origin.x -= arrowButtonImage.size.width + 6.0 @@ -1575,7 +1681,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(nodeLayout.insets.top, separatorHeight)), size: CGSize(width: nodeLayout.contentSize.width, height: separatorHeight)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight)) - strongSelf.separatorNode.isHidden = last + if !item.alwaysShowLastSeparator { + strongSelf.separatorNode.isHidden = last + } if let userPresence = userPresence { strongSelf.peerPresenceManager?.reset(presence: userPresence) @@ -1603,7 +1711,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } @objc private func actionButtonPressed(_ sender: HighlightableButtonNode) { - guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), let item = self.item, index < item.additionalActions.count else { + guard let item = self.item else { + return + } + if let action = item.buttonAction { + action.action?(item.peer, sender, nil) + return + } + guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), index < item.additionalActions.count else { return } item.additionalActions[index].action?(item.peer, sender, nil) diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index ee65de32c0d..909a81bfcd6 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -104,8 +104,9 @@ private final class InnerActionsContainerNode: ASDisplayNode { } } case let .custom(item, _): - itemNodes.append(.custom(item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected))) - if i != items.count - 1 { + let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected) + itemNodes.append(.custom(itemNode)) + if i != items.count - 1 && itemNode.needsSeparator { switch items[i + 1] { case .action, .custom: let separatorNode = ASDisplayNode() @@ -442,6 +443,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { self.targetSelectionIndex = nil icon = nil isUserInteractionEnabled = action != nil + case .videoProcessing: + self.action = nil + self.text = "The video will be published once converted and optimized." + self.targetSelectionIndex = nil + icon = nil + isUserInteractionEnabled = action != nil } self.iconNode = ASImageNode() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 05c6be0606d..0a39323617b 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -226,6 +226,14 @@ public protocol ContextMenuCustomNode: ASDisplayNode { func canBeHighlighted() -> Bool func updateIsHighlighted(isHighlighted: Bool) func performAction() + + var needsSeparator: Bool { get } +} + +public extension ContextMenuCustomNode { + var needsSeparator: Bool { + return true + } } public protocol ContextMenuCustomItem { @@ -2350,6 +2358,7 @@ public final class ContextController: ViewController, StandalonePresentableContr case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) case notificationTopicExceptions(text: String, action: (() -> Void)?) case starsReactions(topCount: Int) + case videoProcessing public static func ==(lhs: Tip, rhs: Tip) -> Bool { switch lhs { @@ -2401,6 +2410,12 @@ public final class ContextController: ViewController, StandalonePresentableContr } else { return false } + case .videoProcessing: + if case .videoProcessing = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index df2715d88d7..cdbeaa9c36c 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -628,7 +628,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C private let requestDismiss: (ContextMenuActionResult) -> Void private var presentationData: PresentationData? - private var itemNode: ContextMenuCustomNode? + private(set) var itemNode: ContextMenuCustomNode? init( getController: @escaping () -> ContextControllerProtocol?, @@ -861,18 +861,28 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio if let separatorNode = item.separatorNode { itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true) + + var separatorHidden = false if i != self.itemNodes.count - 1 { switch self.items[i + 1] { case .separator: - separatorNode.isHidden = true + separatorHidden = true case .action: - separatorNode.isHidden = false + separatorHidden = false case .custom: - separatorNode.isHidden = false + separatorHidden = false } } else { - separatorNode.isHidden = true + separatorHidden = true } + + if let itemContainerNode = item.node as? ContextControllerActionsListCustomItemNode, let itemNode = itemContainerNode.itemNode { + if !itemNode.needsSeparator { + separatorHidden = true + } + } + + separatorNode.isHidden = separatorHidden } itemNodeLayout.apply(itemSize, itemTransition) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 3acfebe45ef..70e4b3f2693 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -795,6 +795,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .animateOut = stateTransition { contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height } + //contentRect.size.height = 200.0 } else { return } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 19de5033840..e4ff26de081 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -70,6 +70,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) case sendNGLogs(PresentationTheme) +// MARK: Nicegram NCG-5828 call recording + case sendNGCallRecorderLogs(PresentationTheme) +// case sendShareLogs case sendGroupCallLogs case sendStorageStats @@ -116,6 +119,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case disableCallV2(Bool) case experimentalCallMute(Bool) case liveStreamV2(Bool) + case dynamicStreaming(Bool) case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) case enableNetworkFramework(Bool) @@ -135,8 +139,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { // case .testStickerImport: return DebugControllerSection.sticker.rawValue - case .sendNGLogs, .sendLogs, .sendOneLog, .sendShareLogs, .sendGroupCallLogs, .sendStorageStats, .sendNotificationLogs, .sendCriticalLogs, .sendAllLogs: +// MARK: Nicegram NCG-5828 call recording, .sendNGCallRecorderLogs + case .sendNGLogs, .sendLogs, .sendOneLog, .sendShareLogs, .sendGroupCallLogs, .sendStorageStats, .sendNotificationLogs, .sendCriticalLogs, .sendAllLogs, .sendNGCallRecorderLogs: return DebugControllerSection.logs.rawValue +// case .accounts: return DebugControllerSection.logs.rawValue case .logToFile, .logToConsole, .redactSensitiveData: @@ -145,7 +151,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2, .dynamicStreaming: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -162,10 +168,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { switch self { // MARK: Nicegram DebugMenu case .showOnboarding: - return -2 + return -3 // case .sendNGLogs: + return -2 +// MARK: Nicegram NCG-5828 call recording + case .sendNGCallRecorderLogs: return -1 +// case .testStickerImport: return 0 case .sendLogs: @@ -270,8 +280,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 52 case .liveStreamV2: return 53 + case .dynamicStreaming: + return 54 case let .preferredVideoCodec(index, _, _, _): - return 54 + index + return 55 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -312,7 +324,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { UIPasteboard.general.setData(data, forPasteboardType: dataType) } - context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) } }) case .sendLogs: @@ -368,7 +380,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -448,7 +460,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(logData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -534,7 +546,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -618,7 +630,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -703,7 +715,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -734,10 +746,62 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) +// MARK: Nicegram NCG-5828 call recording + case .sendNGCallRecorderLogs: + return ItemListDisclosureItem( + presentationData: presentationData, + title: "Send Nicegram Call Recorder Logs", + label: "", + sectionId: self.section, + style: .blocks, action: { + let ngLogs = Logger( + rootPath: arguments.sharedContext.basePath, + basePath: arguments.sharedContext.basePath + "/ngLogs" + ).collectLogs() + + let accountPathName: String? = if let context = arguments.context { + accountRecordIdPathName(context.account.id) + } else { + nil + } + let callRecorderLogs = Logger.shared.collectLogs( + with: arguments.sharedContext.basePath, + accountPathName: accountPathName + ) + + let _ = (callRecorderLogs |> deliverOnMainQueue) + .start(next: { logs in + guard let context = arguments.context else { + return + } + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + controller.peerSelected = { [weak controller] peer, _ in + let peerId = peer.id + + if let strongController = controller { + strongController.dismiss() + + let messages = logs.map { (name, path) -> EnqueueMessage in + let id = Int64.random(in: Int64.min ... Int64.max) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) + return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + } + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() + } + } + arguments.pushController(controller) + }) + }) +// case .sendNGLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Nicegram Logs", label: "", sectionId: self.section, style: .blocks, action: { - let _ = (Logger(rootPath: arguments.sharedContext.basePath, basePath: arguments.sharedContext.basePath + "/ngLogs").collectLogs() - |> deliverOnMainQueue).start(next: { logs in + let ngLogs = Logger( + rootPath: arguments.sharedContext.basePath, + basePath: arguments.sharedContext.basePath + "/ngLogs" + ).collectLogs() + + let _ = (ngLogs |> deliverOnMainQueue) + .start(next: { logs in guard let context = arguments.context else { return } @@ -747,10 +811,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { if let strongController = controller { strongController.dismiss() - + let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() @@ -781,7 +845,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() @@ -890,7 +954,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -945,7 +1009,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(allStatsData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: allStatsData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -1403,6 +1467,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .dynamicStreaming(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Dynamic Streaming", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.dynamicStreaming = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .preferredVideoCodec(_, title, value, isSelected): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1503,6 +1577,9 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.showOnboarding(!AppCache.wasOnboardingShown)) // entries.append(.sendNGLogs(presentationData.theme)) +// MARK: Nicegram NCG-5828 call recording + entries.append(.sendNGCallRecorderLogs(presentationData.theme)) +// let isMainApp = sharedContext.applicationBindings.isMainApp @@ -1582,6 +1659,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.disableCallV2(experimentalSettings.disableCallV2)) entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) + entries.append(.dynamicStreaming(experimentalSettings.dynamicStreaming)) } /*let codecs: [(String, String?)] = [ @@ -1752,7 +1830,7 @@ public func triggerDebugSendLogsUI(context: AccountContext, additionalInfo: Stri let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index cfa335f52dc..1b8b3e4d116 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -100,7 +100,7 @@ public final class DeviceAccess { public static func isCameraAccessAuthorized() -> Bool { return AVCaptureDevice.authorizationStatus(for: .video) == .authorized } - + public static func authorizationStatus(applicationInForeground: Signal? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal { switch subject { case .notifications: diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 7f6756ad146..a3fcbc3fadb 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -54,7 +54,7 @@ public final class DeviceLocationManager: NSObject { self.manager.delegate = self self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters -// self.manager.distanceFilter = 5.0 + self.manager.distanceFilter = kCLDistanceFilterNone self.manager.activityType = .other self.manager.pausesLocationUpdatesAutomatically = false self.manager.headingFilter = 2.0 diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 4ec3efdd878..6b49b014371 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -273,6 +273,24 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditive(layer: CALayer, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.frame = frame + if let completion = completion { + completion(true) + } + case .animated: + let previousFrame = layer.frame + layer.frame = frame + self.animatePositionAdditive(layer: layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) + } + } + } + func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index b01c2a487dc..64801b608e1 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone14ProZoomed case iPhone14ProMax case iPhone14ProMaxZoomed + case iPhone16Pro + case iPhone16ProMax case iPad case iPadMini case iPad102Inch @@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, + .iPhone16Pro, + .iPhone16ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 430.0, height: 932.0) case .iPhone14ProMaxZoomed: return CGSize(width: 375.0, height: 812.0) + case .iPhone16Pro: + return CGSize(width: 402.0, height: 874.0) + case .iPhone16ProMax: + return CGSize(width: 440.0, height: 956.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 + case .iPhone16Pro, .iPhone16ProMax: + return 55.0 case let .unknown(_, _, _, screenCornerRadius): return screenCornerRadius default: @@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? 21.0 : 34.0 case .iPhone14ProZoomed: return inLandscape ? 21.0 : 28.0 @@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 54.0 case .iPhone14ProMaxZoomed: return 47.0 + case .iPhone16Pro, .iPhone16ProMax: + return 54.0 case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: @@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: return 292.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 44.0 case .iPhone6Plus: return 45.0 @@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public var hasDynamicIsland: Bool { switch self { - case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return true default: return false diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 085da48be88..6265a0a42c9 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -482,6 +482,7 @@ public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloa public enum DrawingContextBltMode { case Alpha + case AlphaFromColor } public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings { @@ -751,38 +752,64 @@ public class DrawingContext { let maxDstY = dstY + height switch mode { - case .Alpha: - while dstY < maxDstY { - let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) - let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + case .Alpha: + while dstY < maxDstY { + let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + var dx = dstX + var sx = srcX + while dx < maxDstX { + let srcPixel = srcLine + sx + let dstPixel = dstLine + dx - var dx = dstX - var sx = srcX - while dx < maxDstX { - let srcPixel = srcLine + sx - let dstPixel = dstLine + dx - - let baseColor = dstPixel.pointee - let baseAlpha = (baseColor >> 24) & 0xff - let baseR = (baseColor >> 16) & 0xff - let baseG = (baseColor >> 8) & 0xff - let baseB = baseColor & 0xff - - let alpha = min(baseAlpha, srcPixel.pointee >> 24) - - let r = (baseR * alpha) / 255 - let g = (baseG * alpha) / 255 - let b = (baseB * alpha) / 255 - - dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b - - dx += 1 - sx += 1 - } + let baseColor = dstPixel.pointee + let baseAlpha = (baseColor >> 24) & 0xff + let baseR = (baseColor >> 16) & 0xff + let baseG = (baseColor >> 8) & 0xff + let baseB = baseColor & 0xff - dstY += 1 - srcY += 1 + let alpha = min(baseAlpha, srcPixel.pointee >> 24) + + let r = (baseR * alpha) / 255 + let g = (baseG * alpha) / 255 + let b = (baseB * alpha) / 255 + + dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + dx += 1 + sx += 1 + } + + dstY += 1 + srcY += 1 + } + case .AlphaFromColor: + while dstY < maxDstY { + let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + var dx = dstX + var sx = srcX + while dx < maxDstX { + let srcPixel = srcLine + sx + let dstPixel = dstLine + dx + + let alpha = (srcPixel.pointee >> 0) & 0xff + + let r = alpha + let g = alpha + let b = alpha + + dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + dx += 1 + sx += 1 } + + dstY += 1 + srcY += 1 + } } } } diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index 7957d6fa9b9..8851e6c209e 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -277,6 +277,7 @@ open class ImmediateTextView: TextView { private var linkHighlightingNode: LinkHighlightingNode? public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero public var trailingLineWidth: CGFloat? @@ -356,7 +357,7 @@ open class ImmediateTextView: TextView { } } - if let rects = rects { + if var rects, !rects.isEmpty { let linkHighlightingNode: LinkHighlightingNode if let current = strongSelf.linkHighlightingNode { linkHighlightingNode = current @@ -366,7 +367,8 @@ open class ImmediateTextView: TextView { strongSelf.addSubnode(linkHighlightingNode) } linkHighlightingNode.frame = strongSelf.bounds - linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) + linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { strongSelf.linkHighlightingNode = nil linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in diff --git a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift b/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift deleted file mode 100644 index b8c2ed10c5e..00000000000 --- a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import UIKit - -final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.cancelsTouchesInView = false - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - self.delegate = self - - self.direction = [.left, .right] - self.numberOfTouchesRequired = 2 - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } -} diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 252d5efc546..8072f48881f 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -188,7 +188,7 @@ public final class TextAlertContentNode: AlertContentNode { } } - public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool) { + public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) { self.theme = theme self.actionLayout = actionLayout self._dismissOnOutsideTap = dismissOnOutsideTap @@ -214,6 +214,15 @@ public final class TextAlertContentNode: AlertContentNode { self.textNode.isAccessibilityElement = true self.textNode.accessibilityLabel = text.string self.textNode.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + self.textNode.tapAttributeAction = linkAction + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + self.textNode.linkHighlightColor = theme.accentColor.withMultipliedAlpha(0.1) if text.length != 0 { if let paragraphStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { self.textNode.textAlignment = paragraphStyle.alignment @@ -450,7 +459,7 @@ public func textAlertController(theme: AlertControllerTheme, title: NSAttributed return AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap)) } -public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { +public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { var dismissImpl: (() -> Void)? let attributedText: NSAttributedString if parseMarkdown { @@ -458,7 +467,10 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri let boldFont = title == nil ? Font.bold(theme.baseFontSize) : Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) let body = MarkdownAttributeSet(font: font, textColor: theme.primaryColor) let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.primaryColor) - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center) + let link = MarkdownAttributeSet(font: font, textColor: theme.accentColor) + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { url in + return ("URL", url) + }), textAlignment: .center) } else { attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)), textColor: theme.primaryColor, paragraphAlignment: .center) } @@ -467,7 +479,7 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri dismissImpl?() action.action() }) - }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap), allowInputInset: allowInputInset) + }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction), allowInputInset: allowInputInset) dismissImpl = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 5ddaa0ac018..101f10a7341 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -66,15 +66,23 @@ public enum SourceAndRect { case node(() -> (ASDisplayNode, CGRect)?) case view(() -> (UIView, CGRect)?) - func globalRect() -> CGRect? { + func globalRect(isAlreadyGlobal: Bool) -> CGRect? { switch self { case let .node(node): if let (sourceNode, sourceRect) = node() { - return sourceNode.view.convert(sourceRect, to: nil) + if isAlreadyGlobal { + return sourceRect + } else { + return sourceNode.view.convert(sourceRect, to: nil) + } } case let .view(view): if let (sourceView, sourceRect) = view() { - return sourceView.convert(sourceRect, to: nil) + if isAlreadyGlobal { + return sourceRect + } else { + return sourceView.convert(sourceRect, to: nil) + } } } return nil @@ -83,13 +91,16 @@ public enum SourceAndRect { public final class TooltipControllerPresentationArguments { public let sourceAndRect: SourceAndRect + public let sourceRectIsGlobal: Bool - public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?) { + public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?, sourceRectIsGlobal: Bool = false) { self.sourceAndRect = .node(sourceNodeAndRect) + self.sourceRectIsGlobal = sourceRectIsGlobal } - public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?) { + public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?, sourceRectIsGlobal: Bool = false) { self.sourceAndRect = .view(sourceViewAndRect) + self.sourceRectIsGlobal = sourceRectIsGlobal } } @@ -194,7 +205,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } else { self.layout = layout - if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect() { + if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect(isAlreadyGlobal: presentationArguments.sourceRectIsGlobal) { self.controllerNode.sourceRect = sourceRect } else { self.controllerNode.sourceRect = nil diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index 19416153361..28d569a23a5 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -231,6 +231,16 @@ private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOr } public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if let view = gestureRecognizer.view { + let location = touch.location(in: gestureRecognizer.view) + if location.y > view.bounds.height - 44.0 { + return false + } + } + return true + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -1301,7 +1311,7 @@ public class Window1 { } } - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + @objc func panGesture(_ recognizer: WindowPanRecognizer) { switch recognizer.state { case .began: self.panGestureBegan(location: recognizer.location(in: recognizer.view)) diff --git a/submodules/Display/Source/WindowPanRecognizer.swift b/submodules/Display/Source/WindowPanRecognizer.swift index 53ed3949123..7ef93a5ca6c 100644 --- a/submodules/Display/Source/WindowPanRecognizer.swift +++ b/submodules/Display/Source/WindowPanRecognizer.swift @@ -7,6 +7,7 @@ public final class WindowPanRecognizer: UIGestureRecognizer { public var ended: ((CGPoint, CGPoint?) -> Void)? private var previousPoints: [(CGPoint, Double)] = [] + private var previousVelocity: CGFloat = 0.0 override public func reset() { super.reset() @@ -45,6 +46,11 @@ public final class WindowPanRecognizer: UIGestureRecognizer { } } + func velocity(in view: UIView?) -> CGPoint { + let point = CGPoint(x: 0.0, y: self.previousVelocity) + return self.view?.convert(point, to: view) ?? .zero + } + override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) @@ -68,9 +74,12 @@ public final class WindowPanRecognizer: UIGestureRecognizer { override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) + self.state = .ended + if let touch = touches.first { let location = touch.location(in: self.view) self.addPoint(location) + self.previousVelocity = self.estimateVerticalVelocity() self.ended?(location, CGPoint(x: 0.0, y: self.estimateVerticalVelocity())) } } @@ -78,6 +87,8 @@ public final class WindowPanRecognizer: UIGestureRecognizer { override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) + self.state = .cancelled + if let touch = touches.first { self.ended?(touch.location(in: self.view), nil) } diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 5ef068ce40c..2084221c276 100644 --- a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -209,7 +209,7 @@ public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() } else { - string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: "") + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "HTTP://", with: "").replacingOccurrences(of: "HTTPS://", with: "").replacingOccurrences(of: "TONSITE://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index ae5a1f2e6e5..e73f00da421 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -330,6 +330,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private func setupWithVideo(_ file: TelegramMediaFile) { let videoNode = UniversalVideoNode( + accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, @@ -1140,7 +1141,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -1160,9 +1161,9 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -1220,8 +1221,8 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -1236,7 +1237,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/FFMpegBinding/Package.swift b/submodules/FFMpegBinding/Package.swift index da926bdafc9..6d794a2920e 100644 --- a/submodules/FFMpegBinding/Package.swift +++ b/submodules/FFMpegBinding/Package.swift @@ -27,7 +27,7 @@ let package = Package( publicHeadersPath: "Public", cSettings: [ .headerSearchPath("Public"), - .unsafeFlags(["-I../../../../core-xprojects/ffmpeg/build/ffmpeg/include"]) + .headerSearchPath("SharedHeaders/ffmpeg/include"), ]), ] ) diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h index efcec50cb82..d4e53eaf6b2 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h @@ -44,6 +44,7 @@ extern int FFMpegCodecIdVP9; - (bool)isAttachedPicAtStreamIndex:(int32_t)streamIndex; - (int)codecIdAtStreamIndex:(int32_t)streamIndex; - (double)duration; +- (int64_t)startTimeAtStreamIndex:(int32_t)streamIndex; - (int64_t)durationAtStreamIndex:(int32_t)streamIndex; - (bool)codecParamsAtStreamIndex:(int32_t)streamIndex toContext:(FFMpegAVCodecContext *)context; - (FFMpegFpsAndTimebase)fpsAndTimebaseForStreamIndex:(int32_t)streamIndex defaultTimeBase:(CMTime)defaultTimeBase; diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h index 8f4f8314b19..4eb1d26df77 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h @@ -8,7 +8,7 @@ extern int FFMPEG_CONSTANT_AVERROR_EOF; @interface FFMpegAVIOContext : NSObject -- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable; +- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable; - (void *)impl; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m index bd4a6550158..45dd51550e9 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m @@ -103,6 +103,10 @@ - (double)duration { return (double)_impl->duration / AV_TIME_BASE; } +- (int64_t)startTimeAtStreamIndex:(int32_t)streamIndex { + return _impl->streams[streamIndex]->start_time; +} + - (int64_t)durationAtStreamIndex:(int32_t)streamIndex { return _impl->streams[streamIndex]->duration; } diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m index b0e54a15ac3..a22e467c2b4 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m @@ -12,7 +12,7 @@ @interface FFMpegAVIOContext () { @implementation FFMpegAVIOContext -- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable { +- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable { self = [super init]; if (self != nil) { void *avIoBuffer = av_malloc(bufferSize); diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index cbe260d608d..873b7181124 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -49,7 +49,8 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", - "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", + "//submodules/TelegramUI/Components/SliderContextItem", + "//submodules/TelegramUI/Components/SectionTitleContextItem", "//submodules/TooltipUI", "//submodules/TelegramNotices", "//submodules/Pasteboard", @@ -57,6 +58,15 @@ swift_library( "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + "//submodules/TelegramUI/Components/BadgeComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 127cce67fa3..ce9f9e7b01a 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -123,8 +123,6 @@ class CaptionScrollWrapperNode: ASDisplayNode { } } - - final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -859,7 +857,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } else if let media = media as? TelegramMediaFile, !media.isAnimated { for attribute in media.attributes { switch attribute { - case let .Video(_, dimensions, _, _, _): + case let .Video(_, dimensions, _, _, _, _): isVideo = true if dimensions.height > 0 { if CGFloat(dimensions.width) / CGFloat(dimensions.height) > 1.33 { @@ -959,7 +957,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } else if media is TelegramMediaImage { hasCaption = true } else if let file = media as? TelegramMediaFile { - hasCaption = file.mimeType.hasPrefix("image/") + hasCaption = file.mimeType.hasPrefix("image/") || file.mimeType.hasPrefix("video/") } else if media is TelegramMediaInvoice { hasCaption = true } @@ -1106,8 +1104,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let dustNode = InvisibleInkDustNode(textNode: spoilerTextNode, enableAnimations: self.context.sharedContext.energyUsageSettings.fullTranslucency) self.dustNode = dustNode - spoilerTextNode.supernode?.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) - + if let textSelectionNode = self.textSelectionNode { + spoilerTextNode.supernode?.insertSubnode(dustNode, aboveSubnode: textSelectionNode) + } else { + spoilerTextNode.supernode?.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) + } } if let dustNode = self.dustNode { dustNode.update(size: textFrame.size, color: .white, textColor: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index bee6431d994..b98b543865e 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -9,6 +9,7 @@ import UniversalMediaPlayer import TelegramPresentationData import RangeSet import ShimmerEffect +import TelegramUniversalVideoContent private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index ae0d7a50091..a02c7eb7912 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -248,7 +248,22 @@ public func galleryItemForEntry( content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + var isHLS = false + if NativeVideoContent.isHLSVideo(file: file) { + isHLS = true + + if let data = context.currentAppConfiguration.with({ $0 }).data, let disableHLS = data["video_ignore_alt_documents"] as? Double { + if Int(disableHLS) != 0 { + isHLS = false + } + } + } + + if isHLS { + content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + } else { + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + } } else { content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } @@ -308,13 +323,16 @@ public func galleryItemForEntry( } else { if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) - } - else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { + } else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { var pixelsCount: Int = 0 if let dimensions = file.dimensions { pixelsCount = Int(dimensions.width) * Int(dimensions.height) } - if pixelsCount < 10000 * 10000 { + var fileSize: Int64 = 0 + if let size = file.size { + fileSize = size + } + if pixelsCount < 10000 * 10000 && fileSize < 16 * 1024 * 1024 { return ChatImageGalleryItem( context: context, presentationData: presentationData, @@ -580,6 +598,7 @@ public class GalleryController: ViewController, StandalonePresentableController, private let landscape: Bool private let timecode: Double? private var playbackRate: Double? + private var videoQuality: UniversalVideoContentVideoQuality = .auto private let accountInUseDisposable = MetaDisposable() private let disposable = MetaDisposable() @@ -698,13 +717,8 @@ public class GalleryController: ViewController, StandalonePresentableController, translateToLanguage = chatTranslationState(context: context, peerId: messageId.peerId) |> map { translationState in if let translationState, translationState.isEnabled { - var translateToLanguage = translationState.toLang ?? baseLanguageCode - if translateToLanguage == "nb" { - translateToLanguage = "no" - } else if translateToLanguage == "pt-br" { - translateToLanguage = "pt" - } - return translateToLanguage + let translateToLanguage = translationState.toLang ?? baseLanguageCode + return normalizeTranslationLanguage(translateToLanguage) } else { return nil } @@ -1406,8 +1420,15 @@ public class GalleryController: ViewController, StandalonePresentableController, } } - self.galleryNode.completeCustomDismiss = { [weak self] in - self?._hiddenMedia.set(.single(nil)) + self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in + if isPictureInPicture { + if let chatController = self?.baseNavigationController?.topViewController as? ChatController { + chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0))) + } + } else { + self?._hiddenMedia.set(.single(nil)) + } + self?.presentingViewController?.dismiss(animated: false, completion: nil) } @@ -1759,6 +1780,16 @@ public class GalleryController: ViewController, StandalonePresentableController, } } + func updateSharedVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.videoQuality = videoQuality + + self.galleryNode.pager.forEachItemNode { itemNode in + if let itemNode = itemNode as? UniversalVideoGalleryItemNode { + itemNode.updateVideoQuality(videoQuality) + } + } + } + public var keyShortcuts: [KeyShortcut] { var keyShortcuts: [KeyShortcut] = [] keyShortcuts.append( diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index c0f1930656f..d269eddc5e9 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture public var pager: GalleryPagerNode public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } @@ -123,9 +123,9 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } } - self.pager.completeCustomDismiss = { [weak self] in + self.pager.completeCustomDismiss = { [weak self] isPictureInPicture in if let strongSelf = self { - strongSelf.completeCustomDismiss() + strongSelf.completeCustomDismiss(isPictureInPicture) } } @@ -303,7 +303,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.pager.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size) - self.pager.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + self.pager.containerLayoutUpdated(layout, navigationBarHeight: self.areControlsHidden ? 0.0 : navigationBarHeight, transition: transition) } open func setControlsHidden(_ hidden: Bool, animated: Bool) { @@ -470,6 +470,10 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 let minimalDismissDistance = scrollView.contentSize.height / 12.0 if abs(velocity.y) > 1.0 || abs(distanceFromEquilibrium) > minimalDismissDistance { + if distanceFromEquilibrium > 1.0, let centralItemNode = self.pager.centralItemNode(), centralItemNode.maybePerformActionForSwipeDismiss() { + return + } + if let backgroundColor = self.backgroundNode.backgroundColor { self.backgroundNode.layer.animate(from: backgroundColor, to: UIColor(white: 0.0, alpha: 0.0).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, removeOnCompletion: false) } diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 3cbd05e5a6f..1e3d02dbf17 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -11,6 +11,11 @@ public enum GalleryItemNodeNavigationStyle { } open class GalleryItemNode: ASDisplayNode { + public enum ActiveEdge { + case left + case right + } + private var _index: Int? public var index: Int { get { @@ -25,7 +30,7 @@ open class GalleryItemNode: ASDisplayNode { public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } public var alternativeDismiss: () -> Bool = { return false } @@ -100,6 +105,10 @@ open class GalleryItemNode: ASDisplayNode { open func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { } + open func maybePerformActionForSwipeDismiss() -> Bool { + return false + } + open func contentSize() -> CGSize? { return nil } @@ -107,4 +116,14 @@ open class GalleryItemNode: ASDisplayNode { open var keyShortcuts: [KeyShortcut] { return [] } + + open func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + return false + } + + open func setActiveEdgeAction(edge: ActiveEdge?) { + } + + open func adjustActiveEdgeAction(distance: CGFloat) { + } } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 1ee1b3af555..01d821e7c55 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -9,6 +9,10 @@ private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) } +private func activeEdgeWidth(width: CGFloat) -> CGFloat { + return floor(width * 0.4) +} + let fadeWidth: CGFloat = 70.0 private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -85,6 +89,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest private let leftFadeNode: ASDisplayNode private let rightFadeNode: ASDisplayNode private var highlightedSide: Bool? + private var activeSide: Bool? + private var canPerformSideNavigationAction: Bool = false + private var sideActionInitialPosition: CGPoint? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -110,7 +117,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } @@ -118,6 +125,8 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var pagingEnabledPromise = Promise(true) private var pagingEnabledDisposable: Disposable? + private var edgeLongTapTimer: Foundation.Timer? + public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap self.disableTapNavigation = disableTapNavigation @@ -170,57 +179,99 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest recognizer.delegate = self.wrappedGestureRecognizerDelegate self.tapRecognizer = recognizer recognizer.tapActionAtPoint = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return .fail } let size = strongSelf.bounds var highlightedSide: Bool? - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - if point.y < 80.0 { - highlightedSide = nil - } else { + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { highlightedSide = true } } } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true + } + } - if highlightedSide == nil { + if !strongSelf.pagingEnabled { + highlightedSide = nil + } + + if highlightedSide == nil && activeSide == nil { return .fail } if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { return .fail } - return .keepWithSingleTap + + if activeSide != nil { + return .waitForHold(timeout: 0.3, acceptTap: true) + } else { + return .keepWithSingleTap + } } recognizer.highlight = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return } let size = strongSelf.bounds var highlightedSide: Bool? - if let point = point { - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if let point { + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - highlightedSide = true + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { + highlightedSide = true + } } } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true + } + } + } + + if !strongSelf.pagingEnabled { + highlightedSide = nil } + if strongSelf.highlightedSide != highlightedSide { strongSelf.highlightedSide = highlightedSide + if highlightedSide != nil { + strongSelf.canPerformSideNavigationAction = true + } + let leftAlpha: CGFloat let rightAlpha: CGFloat if let highlightedSide = highlightedSide { @@ -247,6 +298,47 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + + if strongSelf.activeSide != activeSide { + strongSelf.activeSide = activeSide + + if let activeSide, let centralIndex = strongSelf.centralItemIndex, let _ = strongSelf.visibleItemNode(at: centralIndex) { + if strongSelf.edgeLongTapTimer == nil { + strongSelf.edgeLongTapTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { _ in + guard let self else { + return + } + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: activeSide ? .right : .left) + } + + self.canPerformSideNavigationAction = false + + let leftAlpha: CGFloat + let rightAlpha: CGFloat + + leftAlpha = 0.0 + rightAlpha = 0.0 + + if self.leftFadeNode.alpha != leftAlpha { + self.leftFadeNode.alpha = leftAlpha + self.leftFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + if self.rightFadeNode.alpha != rightAlpha { + self.rightFadeNode.alpha = rightAlpha + self.rightFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + }) + } + } else if let edgeLongTapTimer = strongSelf.edgeLongTapTimer { + edgeLongTapTimer.invalidate() + strongSelf.edgeLongTapTimer = nil + + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: nil) + } + } + } } self.view.addGestureRecognizer(recognizer) } @@ -258,8 +350,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: + self.sideActionInitialPosition = nil if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - if case .tap = gesture { + if case .tap = gesture, self.canPerformSideNavigationAction { let size = self.bounds.size if location.x < edgeWidth(width: size.width) && self.canGoToPreviousItem() { self.goToPreviousItem() @@ -268,6 +361,21 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + case .cancelled: + self.sideActionInitialPosition = nil + case .began: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + self.sideActionInitialPosition = location + } + case .changed: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + if let sideActionInitialPosition = self.sideActionInitialPosition { + let distance = location.x - sideActionInitialPosition.x + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.adjustActiveEdgeAction(distance: distance) + } + } + } default: break } diff --git a/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift new file mode 100644 index 00000000000..e596760fc24 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle + +final class GalleryRateToastAnimationComponent: Component { + let speedFraction: CGFloat + + init(speedFraction: CGFloat) { + self.speedFraction = speedFraction + } + + static func ==(lhs: GalleryRateToastAnimationComponent, rhs: GalleryRateToastAnimationComponent) -> Bool { + if lhs.speedFraction != rhs.speedFraction { + return false + } + return true + } + + final class View: UIView { + private let itemViewContainer: UIView + private var itemViews: [UIImageView] = [] + + private var link: SharedDisplayLinkDriver.Link? + private var timeValue: CGFloat = 0.0 + private var speedFraction: CGFloat = 1.0 + + override init(frame: CGRect) { + self.itemViewContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.itemViewContainer) + + let image = UIImage(bundleImageName: "Media Gallery/VideoRateToast")?.withRenderingMode(.alwaysTemplate) + for _ in 0 ..< 2 { + let itemView = UIImageView(image: image) + itemView.tintColor = .white + self.itemViews.append(itemView) + self.itemViewContainer.addSubview(itemView) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.link?.invalidate() + } + + private func setupAnimations() { + if self.link == nil { + var previousTimestamp = CACurrentMediaTime() + self.link = SharedDisplayLinkDriver.shared.add { [weak self] _ in + guard let self else { + return + } + + let timestamp = CACurrentMediaTime() + let deltaMultiplier = 1.0 * (1.0 - self.speedFraction) + 3.0 * self.speedFraction + let deltaTime = (timestamp - previousTimestamp) * deltaMultiplier + previousTimestamp = timestamp + + self.timeValue += deltaTime + + let duration: CGFloat = 1.2 + + for i in 0 ..< self.itemViews.count { + var itemFraction = (self.timeValue + CGFloat(i) * 0.1).truncatingRemainder(dividingBy: duration) / duration + + if itemFraction >= 0.5 { + itemFraction = (1.0 - itemFraction) / 0.5 + } else { + itemFraction = itemFraction / 0.5 + } + + let itemAlpha = 0.6 * (1.0 - itemFraction) + 1.0 * itemFraction + let itemScale = 0.9 * (1.0 - itemFraction) + 1.1 * itemFraction + + self.itemViews[i].alpha = itemAlpha + self.itemViews[i].transform = CGAffineTransformMakeScale(itemScale, itemScale) + } + } + } + } + + func update(component: GalleryRateToastAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.speedFraction = component.speedFraction + + let itemSize = self.itemViews[0].image?.size ?? CGSize(width: 10.0, height: 10.0) + let itemSpacing: CGFloat = 1.0 + + let size = CGSize(width: itemSize.width * 2.0 + itemSpacing, height: 12.0) + + for i in 0 ..< self.itemViews.count { + let itemFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (itemSize.width + itemSpacing), y: UIScreenPixel), size: itemSize) + self.itemViews[i].center = itemFrame.center + self.itemViews[i].bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + + self.itemViews[i].layer.speed = Float(1.0 * (1.0 - component.speedFraction) + 2.0 * component.speedFraction) + } + + self.setupAnimations() + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift new file mode 100644 index 00000000000..caadc1e407c --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift @@ -0,0 +1,234 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import BalancedTextComponent +import AnimatedTextComponent +import LottieComponent + +final class GalleryRateToastComponent: Component { + let rate: Double + let displayTooltip: String? + + init(rate: Double, displayTooltip: String?) { + self.rate = rate + self.displayTooltip = displayTooltip + } + + static func ==(lhs: GalleryRateToastComponent, rhs: GalleryRateToastComponent) -> Bool { + if lhs.rate != rhs.rate { + return false + } + if lhs.displayTooltip != rhs.displayTooltip { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let text = ComponentView() + private let arrows = ComponentView() + + private var tooltipText: ComponentView? + private var tooltipAnimation: ComponentView? + + private var tooltipIsHidden: Bool = false + private var tooltipTimer: Foundation.Timer? + + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.tooltipTimer?.invalidate() + } + + func update(component: GalleryRateToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.state = state + + let insets = UIEdgeInsets(top: 5.0, left: 11.0, bottom: 5.0, right: 16.0) + let spacing: CGFloat = 5.0 + + var rateString = String(format: "%.1f", component.rate) + if rateString.hasSuffix(".0") { + rateString = rateString.replacingOccurrences(of: ".0", with: "") + } + + var textItems: [AnimatedTextComponent.Item] = [] + if let dotRange = rateString.range(of: ".") { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(String(rateString[rateString.startIndex ..< dotRange.lowerBound])))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("dot"), content: .text("."))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("post"), content: .text(String(rateString[dotRange.upperBound...])))) + } else { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(rateString))) + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("x"), content: .text("x"))) + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 17.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), + color: .white, + items: textItems + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + var speedFraction = (component.rate - 1.0) / (2.5 - 1.0) + speedFraction = max(0.0, min(1.0, speedFraction)) + let arrowsSize = self.arrows.update( + transition: transition, + component: AnyComponent(GalleryRateToastAnimationComponent(speedFraction: speedFraction)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + + let size = CGSize(width: insets.left + insets.right + textSize.width + arrowsSize.width, height: insets.top + insets.bottom + max(textSize.height, arrowsSize.height)) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 0.0, alpha: 0.5), + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - size.width) * 0.5), y: 0.0), size: size) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + insets.left, y: backgroundFrame.minY + floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.origin) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + let arrowsFrame = CGRect(origin: CGPoint(x: textFrame.maxX + spacing, y: backgroundFrame.minY + floorToScreenPixels((size.height - arrowsSize.height) * 0.5)), size: arrowsSize) + if let arrowsView = self.arrows.view { + if arrowsView.superview == nil { + self.addSubview(arrowsView) + } + transition.setFrame(view: arrowsView, frame: arrowsFrame) + } + + if let displayTooltip = component.displayTooltip { + var tooltipTransition = transition + + let tooltipText: ComponentView + if let current = self.tooltipText { + tooltipText = current + } else { + tooltipText = ComponentView() + self.tooltipText = tooltipText + tooltipTransition = tooltipTransition.withAnimation(.none) + } + + let tooltipAnimation: ComponentView + if let current = self.tooltipAnimation { + tooltipAnimation = current + } else { + tooltipAnimation = ComponentView() + self.tooltipAnimation = tooltipAnimation + } + + let tooltipTextSize = tooltipText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: displayTooltip, font: Font.regular(15.0), textColor: UIColor(white: 1.0, alpha: 0.8))), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 1000.0) + ) + let tooltipTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tooltipTextSize.width) * 0.5), y: backgroundFrame.maxY + 10.0), size: tooltipTextSize) + if let tooltipTextView = tooltipText.view { + if tooltipTextView.superview == nil { + self.addSubview(tooltipTextView) + } + tooltipTransition.setPosition(view: tooltipTextView, position: tooltipTextFrame.center) + tooltipTextView.bounds = CGRect(origin: CGPoint(), size: tooltipTextFrame.size) + + transition.setAlpha(view: tooltipTextView, alpha: self.tooltipIsHidden ? 0.0 : 1.0) + } + + let tooltipAnimationSize = tooltipAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "video_toast_speedup"), + color: .white, + startingPosition: .begin, + loop: true + )), + environment: {}, + containerSize: CGSize(width: 60.0, height: 60.0) + ) + let tooltipAnimationFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tooltipAnimationSize.width) * 0.5), y: tooltipTextFrame.maxY + 10.0), size: tooltipAnimationSize) + if let tooltipAnimationView = tooltipAnimation.view { + if tooltipAnimationView.superview == nil { + self.addSubview(tooltipAnimationView) + } + tooltipTransition.setFrame(view: tooltipAnimationView, frame: tooltipAnimationFrame) + + transition.setAlpha(view: tooltipAnimationView, alpha: self.tooltipIsHidden ? 0.0 : 0.8) + } + } else { + if let tooltipText = self.tooltipText { + self.tooltipText = nil + if let tooltipTextView = tooltipText.view { + transition.setAlpha(view: tooltipTextView, alpha: 0.0, completion: { [weak tooltipTextView] _ in + tooltipTextView?.removeFromSuperview() + }) + } + } + if let tooltipAnimation = self.tooltipAnimation { + self.tooltipAnimation = nil + if let tooltipAnimationView = tooltipAnimation.view { + transition.setAlpha(view: tooltipAnimationView, alpha: 0.0, completion: { [weak tooltipAnimationView] _ in + tooltipAnimationView?.removeFromSuperview() + }) + } + } + } + + if self.tooltipTimer == nil { + self.tooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.tooltipIsHidden = true + self.state?.updated(transition: .easeInOut(duration: 0.25), isLocal: true) + }) + } + + return CGSize(width: availableSize.width, height: size.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift b/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift index ac935299262..65fede29215 100644 --- a/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift +++ b/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift @@ -14,7 +14,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -34,9 +34,9 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -94,8 +94,8 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -110,7 +110,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index bb911f25ea8..0ea96bd194f 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -552,7 +552,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { }, iconSource: nil, action: { [weak self] _, f in f(.dismissWithoutContent) if let navigationController = self?.baseNavigationController() as? NavigationController { - navigationController.pushViewController(AdsInfoScreen(context: context)) + navigationController.pushViewController(AdsInfoScreen(context: context, mode: .channel)) } }))) @@ -687,7 +687,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + self.completeCustomDismiss(false) } } f(.default) @@ -719,7 +719,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + self.completeCustomDismiss(false) } } f(.default) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 074f443cdaa..f3a46f5fbc5 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -27,6 +27,12 @@ import Pasteboard import AdUI import AdsInfoScreen import AdsReportScreen +import SaveProgressScreen +import SectionTitleContextItem +import RasterizedCompositionComponent +import BadgeComponent +import ComponentFlow +import ComponentDisplayAdapters public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -502,6 +508,228 @@ final class MoreHeaderButton: HighlightableButtonNode { } } +final class SettingsHeaderButton: HighlightableButtonNode { + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + + private let iconLayer: RasterizedCompositionMonochromeLayer + + private let gearsLayer: RasterizedCompositionImageLayer + private let dotLayer: RasterizedCompositionImageLayer + + private var speedBadge: ComponentView? + private var qualityBadge: ComponentView? + + private var speedBadgeText: String? + private var qualityBadgeText: String? + + private let badgeFont: UIFont + + private var isMenuOpen: Bool = false + + var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private let wide: Bool + + init(wide: Bool = false) { + self.wide = wide + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.iconLayer = RasterizedCompositionMonochromeLayer() + //self.iconLayer.backgroundColor = UIColor.green.cgColor + + self.gearsLayer = RasterizedCompositionImageLayer() + self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) + + self.dotLayer = RasterizedCompositionImageLayer() + self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) + + self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) + self.iconLayer.contentsLayer.addSublayer(self.dotLayer) + + self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) + + super.init() + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.layer.addSublayer(self.iconLayer) + self.addSubnode(self.containerNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let _ = strongSelf.contextAction else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0)) + self.referenceNode.frame = self.containerNode.bounds + + self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0) + + if let image = self.gearsLayer.image { + let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) + let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) + let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - iconSize.width) / 2.0), y: floor((self.containerNode.bounds.height - iconSize.height) / 2.0)), size: iconSize) + self.iconLayer.position = iconFrame.center + self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor + + let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) + self.gearsLayer.position = gearsFrame.center + self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) + + if let dotImage = self.dotLayer.image { + let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) + self.dotLayer.position = dotFrame.center + self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) + } + } + } + + override func didLoad() { + super.didLoad() + self.view.isOpaque = false + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: wide ? 32.0 : 22.0, height: 44.0) + } + + func onLayout() { + } + + func setIsMenuOpen(isMenuOpen: Bool) { + if self.isMenuOpen == isMenuOpen { + return + } + self.isMenuOpen = isMenuOpen + + let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) + self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + + self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + } + + func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { + if self.speedBadgeText == speed && self.qualityBadgeText == quality { + return + } + self.speedBadgeText = speed + self.qualityBadgeText = quality + + if let badgeText = speed { + var badgeTransition = transition + let speedBadge: ComponentView + if let current = self.speedBadge { + speedBadge = current + } else { + speedBadge = ComponentView() + self.speedBadge = speedBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = speedBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: 3.0, + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let speedBadgeView = speedBadge.view { + if speedBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) + + transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) + } + } else if let speedBadge = self.speedBadge { + self.speedBadge = nil + if let speedBadgeView = speedBadge.view { + let speedBadgeLayer = speedBadgeView.layer + transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in + speedBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: speedBadgeLayer, scale: 0.001) + } + } + + if let badgeText = quality { + var badgeTransition = transition + let qualityBadge: ComponentView + if let current = self.qualityBadge { + qualityBadge = current + } else { + qualityBadge = ComponentView() + self.qualityBadge = qualityBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = qualityBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: 3.0, + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let qualityBadgeView = qualityBadge.view { + if qualityBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) + + transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) + } + } else if let qualityBadge = self.qualityBadge { + self.qualityBadge = nil + if let qualityBadgeView = qualityBadge.view { + let qualityBadgeLayer = qualityBadgeView.layer + transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in + qualityBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: qualityBadgeLayer, scale: 0.001) + } + } + } +} + @available(iOS 15.0, *) private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate { private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { @@ -604,6 +832,8 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte private var hiddenMediaManagerIndex: Int? private var messageRemovedDisposable: Disposable? + + private var isNativePictureInPictureActiveDisposable: Disposable? init(context: AccountContext, overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { self.overlayController = overlayController @@ -617,30 +847,84 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte super.init() - let contentDelegate = PlaybackDelegate(node: self.node) - self.contentDelegate = contentDelegate + if let videoLayer = videoNode.getVideoLayer() { + let contentDelegate = PlaybackDelegate(node: self.node) + self.contentDelegate = contentDelegate + + let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate)) + self.pictureInPictureController = pictureInPictureController + contentDelegate.pictureInPictureController = pictureInPictureController + + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + pictureInPictureController.requiresLinearPlayback = !canSkip + pictureInPictureController.delegate = self + self.pictureInPictureController = pictureInPictureController + let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in + guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else { + return + } + if pictureInPictureController.isPictureInPicturePossible { + strongSelf.pictureInPictureTimer?.invalidate() + strongSelf.pictureInPictureTimer = nil + + pictureInPictureController.startPictureInPicture() + } + }, queue: .mainQueue()) + self.pictureInPictureTimer = timer + timer.start() + } else { + var currentIsNativePictureInPictureActive = false + self.isNativePictureInPictureActiveDisposable = (videoNode.isNativePictureInPictureActive + |> deliverOnMainQueue).startStrict(next: { [weak self] isNativePictureInPictureActive in + guard let self else { + return + } + + if currentIsNativePictureInPictureActive == isNativePictureInPictureActive { + return + } + currentIsNativePictureInPictureActive = isNativePictureInPictureActive + + if isNativePictureInPictureActive { + Queue.mainQueue().after(0.0, { [weak self] in + guard let self else { + return + } + self.willBegin(self) + + if let overlayController = self.overlayController { + overlayController.setPictureInPictureContentHidden(content: self, isHidden: true) + } + + self.didEnd(self) + }) + } else { + self.expand { [weak self] in + guard let self else { + return + } - let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoNode.getVideoLayer()!, playbackDelegate: contentDelegate)) - self.pictureInPictureController = pictureInPictureController - contentDelegate.pictureInPictureController = pictureInPictureController - - pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false - pictureInPictureController.requiresLinearPlayback = !canSkip - pictureInPictureController.delegate = self - self.pictureInPictureController = pictureInPictureController - let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in - guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else { - return - } - if pictureInPictureController.isPictureInPicturePossible { - strongSelf.pictureInPictureTimer?.invalidate() - strongSelf.pictureInPictureTimer = nil + self.didExpand = true - pictureInPictureController.startPictureInPicture() - } - }, queue: .mainQueue()) - self.pictureInPictureTimer = timer - timer.start() + if let overlayController = self.overlayController { + overlayController.setPictureInPictureContentHidden(content: self, isHidden: false) + self.node.alpha = 0.02 + } + + guard let overlayController = self.overlayController else { + return + } + overlayController.removePictureInPictureContent(content: self) + self.node.canAttachContent = false + if self.didExpand { + return + } + self.node.continuePlayingWithoutSound() + } + } + }) + let _ = videoNode.enterNativePictureInPicture() + } if let hiddenMedia = hiddenMedia { self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia) @@ -676,6 +960,7 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte deinit { self.messageRemovedDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() self.pictureInPictureTimer?.invalidate() self.node.setCanPlaybackWithoutHierarchy(false) @@ -743,10 +1028,257 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte } completionHandler(true) + } + } +} + +@available(iOS 15.0, *) +private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate { + private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { + private let node: UniversalVideoNode + private var statusDisposable: Disposable? + private var status: MediaPlayerStatus? + weak var pictureInPictureController: AVPictureInPictureController? + + private var previousIsPlaying = false + init(node: UniversalVideoNode) { + self.node = node + + super.init() + + var invalidatedStateOnce = false + self.statusDisposable = (self.node.status + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + strongSelf.status = status + if let status { + let isPlaying = status.status == .playing + if !invalidatedStateOnce { + invalidatedStateOnce = true + strongSelf.pictureInPictureController?.invalidatePlaybackState() + } else if strongSelf.previousIsPlaying != isPlaying { + strongSelf.previousIsPlaying = isPlaying + strongSelf.pictureInPictureController?.invalidatePlaybackState() + } + } + }).strict() + } + + deinit { + self.statusDisposable?.dispose() + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { + self.node.togglePlayPause() + } + + public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { + guard let status = self.status else { + return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0))) + } + return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration - status.timestamp, preferredTimescale: CMTimeScale(30.0))) + } + + public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + guard let status = self.status else { + return false + } + switch status.status { + case .playing: + return false + case .buffering, .paused: + return true + } + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { + let node = self.node + let _ = (self.node.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak node] status in + if let node = node, let timestamp = status?.timestamp, let duration = status?.duration { + let nextTimestamp = timestamp + skipInterval.seconds + if nextTimestamp > duration { + node.seek(0.0) + node.pause() + } else { + node.seek(min(duration, nextTimestamp)) + } + } + + completionHandler() + }) + } + + public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + } + + private let context: AccountContext + private let accountId: AccountRecordId + private let hiddenMedia: (MessageId, Media)? + private weak var mediaManager: MediaManager? + private var pictureInPictureController: AVPictureInPictureController? + private var contentDelegate: PlaybackDelegate? + private let node: UniversalVideoNode + private let willBegin: (NativePictureInPictureContentImpl) -> Void + private let didBegin: (NativePictureInPictureContentImpl) -> Void + private let didEnd: (NativePictureInPictureContentImpl) -> Void + private let expand: (@escaping () -> Void) -> Void + private var pictureInPictureTimer: SwiftSignalKit.Timer? + private var didExpand: Bool = false + + private var hiddenMediaManagerIndex: Int? + + private var messageRemovedDisposable: Disposable? + + private var isNativePictureInPictureActiveDisposable: Disposable? + + init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didEnd: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { + self.context = context + self.mediaManager = mediaManager + self.accountId = accountId + self.hiddenMedia = hiddenMedia + self.node = videoNode + self.willBegin = willBegin + self.didBegin = didBegin + self.didEnd = didEnd + self.expand = expand + + super.init() + + if let videoLayer = videoNode.getVideoLayer() { + let contentDelegate = PlaybackDelegate(node: self.node) + self.contentDelegate = contentDelegate + + let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate)) + self.pictureInPictureController = pictureInPictureController + contentDelegate.pictureInPictureController = pictureInPictureController + + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + pictureInPictureController.requiresLinearPlayback = !canSkip + pictureInPictureController.delegate = self + self.pictureInPictureController = pictureInPictureController + } + + if let (messageId, _) = hiddenMedia { + var hadMessage: Bool? + self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> map { message -> Bool in + if let _ = message { + return true + } else { + return false + } + } + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + if let hadMessage, hadMessage { + if value { + } else { + if let pictureInPictureController = self.pictureInPictureController { + pictureInPictureController.stopPictureInPicture() + } + } + + return + } + hadMessage = value + }) + } + } + + deinit { + self.messageRemovedDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() + self.pictureInPictureTimer?.invalidate() + self.node.setCanPlaybackWithoutHierarchy(false) + + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager { + mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + } + } + + func updateIsCentral(isCentral: Bool) { + guard let pictureInPictureController = self.pictureInPictureController else { + return + } + + if isCentral { + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true + } else { + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + } + } + + func beginPictureInPicture() { + guard let pictureInPictureController = self.pictureInPictureController else { + return + } + if pictureInPictureController.isPictureInPicturePossible { + pictureInPictureController.startPictureInPicture() + } + } + + func invalidatePlaybackState() { + self.pictureInPictureController?.invalidatePlaybackState() + } + + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.node.setCanPlaybackWithoutHierarchy(true) + + if let hiddenMedia = self.hiddenMedia, let mediaManager = self.mediaManager { + let accountId = self.accountId + self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia) + |> map { messageIdAndMedia in + if let (messageId, media) = messageIdAndMedia { + return .chat(accountId, messageId, media) + } else { + return nil + } + }) + } + + self.willBegin(self) + } + + public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.didBegin(self) + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { + print(error) + } + + public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + } + + public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.node.setCanPlaybackWithoutHierarchy(false) + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager { + mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + self.hiddenMediaManagerIndex = nil + } + self.didEnd(self) + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + self.expand { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.didExpand = true - /*Queue.mainQueue().after(0.2, { - self?.node.canAttachContent = false - })*/ + completionHandler(true) } } } @@ -769,6 +1301,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? + private let settingsBarButton: SettingsHeaderButton + private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false private var videoFramePreview: FramePreview? @@ -785,7 +1319,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var playOnContentOwnership = false private var skipInitialPause = false private var ignorePauseStatus = false - private var validLayout: (ContainerViewLayout, CGFloat)? + private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? private var didPause = false private var isPaused = true private var dismissOnOrientationChange = false @@ -798,10 +1332,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var item: UniversalVideoGalleryItem? private var playbackRate: Double? + private var videoQuality: UniversalVideoContentVideoQuality = .auto private let playbackRatePromise = ValuePromise() + private let videoQualityPromise = ValuePromise() private let statusDisposable = MetaDisposable() private let moreButtonStateDisposable = MetaDisposable() + private let settingsButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -817,14 +1354,23 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) + private let isShowingSettingsMenuPromise = ValuePromise(false, ignoreRepeated: true) private let hasExpandedCaptionPromise = Promise() private var hideControlsDisposable: Disposable? + private var automaticPictureInPictureDisposable: Disposable? var playbackCompleted: (() -> Void)? private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? private var pictureInPictureContent: AnyObject? + private var nativePictureInPictureContent: AnyObject? + + private var activePictureInPictureNavigationController: NavigationController? + private var activePictureInPictureController: ViewController? + + private var activeEdgeRateState: (initialRate: Double, currentRate: Double)? + private var activeEdgeRateIndicator: ComponentView? init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context @@ -849,19 +1395,26 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) + self.settingsBarButton = SettingsHeaderButton() + self.settingsBarButton.isUserInteractionEnabled = true + super.init() self.clipsToBounds = true self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) + self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) } self.overlayContentNode.action = { [weak self] toLandscape in - self?.updateControlsVisibility(!toLandscape) - self?.updateOrientation(toLandscape ? .landscapeRight : .portrait) + guard let self else { + return + } + self.updateControlsVisibility(!toLandscape) + self.updateOrientation(toLandscape ? .landscapeRight : .portrait) } self.statusButtonNode.addSubnode(self.statusNode) @@ -874,7 +1427,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !strongSelf.isPaused { strongSelf.didPause = true } - strongSelf.videoNode?.togglePlayPause() } } @@ -966,15 +1518,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in - self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture) + guard let self else { + return + } + self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false) } self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) - let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.hasExpandedCaptionPromise.get()) - |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, hasExpandedCaptionPromise -> Signal in - if isShowingContextMenu || hasExpandedCaptionPromise { + let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.hasExpandedCaptionPromise.get()) + |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, hasExpandedCaptionPromise -> Signal in + if isShowingContextMenu || isShowingSettingsMenu || hasExpandedCaptionPromise { return .complete() } if isPlaying && !isInteracting && controlsVisible { @@ -996,9 +1551,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() self.moreButtonStateDisposable.dispose() + self.settingsButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() + self.automaticPictureInPictureDisposable?.dispose() } override func ready() -> Signal { @@ -1006,6 +1563,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } + override func contentTapAction() -> Bool { + if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { + self.item?.performAction(.ad(message.id)) + return true + } + return false + } + override func screenFrameUpdated(_ frame: CGRect) { let center = frame.midX - self.frame.width / 2.0 self.subnodeTransform = CATransform3DMakeTranslation(-center * 0.16, 0.0, 0.0) @@ -1042,6 +1607,47 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.updateLayout(placeholderSize, transition: transition) } } + + if let activeEdgeRateState = self.activeEdgeRateState { + var activeEdgeRateIndicatorTransition = transition + let activeEdgeRateIndicator: ComponentView + if let current = self.activeEdgeRateIndicator { + activeEdgeRateIndicator = current + } else { + activeEdgeRateIndicator = ComponentView() + self.activeEdgeRateIndicator = activeEdgeRateIndicator + activeEdgeRateIndicatorTransition = .immediate + } + + let activeEdgeRateIndicatorSize = activeEdgeRateIndicator.update( + transition: ComponentTransition(activeEdgeRateIndicatorTransition), + component: AnyComponent(GalleryRateToastComponent( + rate: activeEdgeRateState.currentRate, + displayTooltip: self.presentationData.strings.Gallery_ToastVideoSpeedSwipe + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: 100.0) + ) + let activeEdgeRateIndicatorFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - activeEdgeRateIndicatorSize.width) * 0.5), y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 8.0), size: activeEdgeRateIndicatorSize) + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + if activeEdgeRateIndicatorView.superview == nil { + self.view.addSubview(activeEdgeRateIndicatorView) + transition.animateTransformScale(view: activeEdgeRateIndicatorView, from: 0.001) + if transition.isAnimated { + activeEdgeRateIndicatorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + activeEdgeRateIndicatorTransition.updateFrame(view: activeEdgeRateIndicatorView, frame: activeEdgeRateIndicatorFrame) + } + } else if let activeEdgeRateIndicator = self.activeEdgeRateIndicator { + self.activeEdgeRateIndicator = nil + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + transition.updateAlpha(layer: activeEdgeRateIndicatorView.layer, alpha: 0.0, completion: { [weak activeEdgeRateIndicatorView] _ in + activeEdgeRateIndicatorView?.removeFromSuperview() + }) + transition.updateTransformScale(layer: activeEdgeRateIndicatorView.layer, scale: 0.001) + } + } if dismiss { self.dismiss() @@ -1096,6 +1702,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var hasLinkedStickers = false if let content = item.content as? NativeVideoContent { hasLinkedStickers = content.fileReference.media.hasLinkedStickers + } else if let content = item.content as? HLSVideoContent { + hasLinkedStickers = content.fileReference.media.hasLinkedStickers } var disablePictureInPicture = false @@ -1104,6 +1712,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var forceEnableUserInteraction = false var isAnimated = false var isEnhancedWebPlayer = false + var isAdaptive = false if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference) @@ -1127,8 +1736,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else if let _ = item.content as? PlatformVideoContent { disablePlayerControls = true forceEnablePiP = true + } else if let _ = item.content as? HLSVideoContent { + isAdaptive = true } + let _ = isAdaptive + let dimensions = item.content.dimensions if dimensions.height > 0.0 { if dimensions.width / dimensions.height < 1.33 || isAnimated { @@ -1147,7 +1760,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoScale: CGFloat if item.content is WebEmbedVideoContent { videoScale = 1.0 @@ -1155,7 +1768,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoScale = 2.0 } let videoSize = CGSize(width: item.content.dimensions.width * videoScale, height: item.content.dimensions.height * videoScale) - videoNode.updateLayout(size: videoSize, transition: .immediate) + let actualVideoSize = CGSize(width: item.content.dimensions.width, height: item.content.dimensions.height) + videoNode.updateLayout(size: videoSize, actualSize: actualVideoSize, transition: .immediate) videoNode.ownsContentNodeUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateDisplayPlaceholder(!value) @@ -1174,6 +1788,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.videoNode?.setBaseRate(playbackRate) } } + + if strongSelf.nativePictureInPictureContent == nil { + strongSelf.setupNativePictureInPicture() + } } } self.videoNode = videoNode @@ -1241,13 +1859,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if let file = file { for attribute in file.attributes { - if case let .Video(duration, _, _, _, _) = attribute, duration >= 30 { + if case let .Video(duration, _, _, _, _, _) = attribute, duration >= 30 { hintSeekable = true break } } let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file) - if !isWebpage { + if !isWebpage && message.adAttribute == nil && !NativeVideoContent.isHLSVideo(file: file) { scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size) } @@ -1265,43 +1883,45 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(), self.playbackRatePromise.get(), - self.isShowingContextMenuPromise.get() - ).start(next: { [weak self] playbackRate, isShowingContextMenu in - guard let strongSelf = self else { + self.videoQualityPromise.get() + ).start(next: { [weak self] playbackRate, videoQuality in + guard let self else { return } - - let effectiveBaseRate: Double - if isShowingContextMenu { - effectiveBaseRate = 1.0 - } else { - effectiveBaseRate = playbackRate + + var rateString: String? + if abs(playbackRate - 1.0) > 0.05 { + var stringValue = String(format: "%.1fx", playbackRate) + if stringValue.hasSuffix(".0x") { + stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x") + } + rateString = stringValue } - - if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 { - strongSelf.moreBarButtonRate = effectiveBaseRate - let animated: Bool - if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp { - animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2) + + var qualityString: String? + if case let .quality(quality) = videoQuality { + if quality <= 360 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityLow + } else if quality <= 480 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityMedium + } else if quality <= 720 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityHD + } else if quality <= 1080 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityFHD } else { - animated = false + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityQHD } - strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() + } - if abs(effectiveBaseRate - 1.0) > 0.01 { - var stringValue = String(format: "%.1fx", effectiveBaseRate) - if stringValue.hasSuffix(".0x") { - stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x") - } - strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated) - } else { - strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated) - } - } else { - if strongSelf.moreBarButtonRateTimestamp == nil { - strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() - } + self.settingsBarButton.setBadges(speed: rateString, quality: qualityString, transition: .spring(duration: 0.35)) + })) + + self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in + guard let self else { + return } + self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu) })) self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) @@ -1441,6 +2061,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers barButtonItems.append(rightBarButtonItem) } + if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_PictureInPicture @@ -1485,6 +2106,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { hasMoreButton = true } + if !isAnimated && !disablePlayerControls { + let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! + settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title + barButtonItems.append(settingsMenuItem) + } + if hasMoreButton { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! moreMenuItem.accessibilityLabel = self.presentationData.strings.Common_More @@ -1532,11 +2159,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let _ = item.content as? NativeVideoContent { self.playbackRate = item.playbackRate() + } else if let _ = item.content as? HLSVideoContent { + self.playbackRate = item.playbackRate() } else if let _ = item.content as? WebEmbedVideoContent { self.playbackRate = item.playbackRate() } self.playbackRatePromise.set(self.playbackRate ?? 1.0) + self.videoQualityPromise.set(self.videoQuality) var isAd = false if let contentInfo = item.contentInfo { @@ -1602,6 +2232,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if isLocal || isStreamable { return true } + } else if let item = self.item, let _ = item.content as? HLSVideoContent { + return true } else if let item = self.item, let _ = item.content as? PlatformVideoContent { return true } @@ -1619,6 +2251,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isAnimated = false if let item = self.item, let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated + } else if let item = self.item, let content = item.content as? HLSVideoContent { + isAnimated = content.fileReference.media.isAnimated } self.hideStatusNodeUntilCentrality = false @@ -1651,6 +2285,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } + + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.updateIsCentral(isCentral: isCentral) + } + } } } @@ -1712,6 +2352,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let time = item.timecode { seek = .timecode(time) } + } else if let content = item.content as? HLSVideoContent { + isAnimated = content.fileReference.media.isAnimated + if let time = item.timecode { + seek = .timecode(time) + } } else if let _ = item.content as? WebEmbedVideoContent { if let time = item.timecode { seek = .timecode(time) @@ -1743,6 +2388,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 { return .loop } + if !item.isSecret, let content = item.content as? HLSVideoContent, content.duration <= 30 { + return .loop + } } return .stop } @@ -2000,7 +2648,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if videoNode.hasAttachedContext { if self.isPaused || !self.keepSoundOnDismiss { - videoNode.continuePlayingWithoutSound() + if let item = self.item, item.content is HLSVideoContent { + } else { + videoNode.continuePlayingWithoutSound() + } } } } else { @@ -2107,6 +2758,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } + override func maybePerformActionForSwipeDismiss() -> Bool { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_swipe_pip"] { + return false + } + + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.beginPictureInPicture() + return true + } + } + return false + } + override func title() -> Signal { return self._title.get() } @@ -2153,7 +2818,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let baseNavigationController = self.baseNavigationController() let mediaManager = self.context.sharedContext.mediaManager var expandImpl: (() -> Void)? - let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { + let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) @@ -2220,17 +2885,133 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } - if customUnembedWhenPortrait(overlayNode) { - self.beginCustomDismiss(false) - self.statusNode.isHidden = true - self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() + if customUnembedWhenPortrait(overlayNode) { + self.beginCustomDismiss(false) + self.statusNode.isHidden = true + self.animateOut(toOverlay: overlayNode, completion: { [weak self] in + self?.completeCustomDismiss(false) + }) + } + } + } + + private func setupNativePictureInPicture() { + guard let item = self.item, let videoNode = self.videoNode else { + return + } + + if videoNode.getVideoLayer() == nil { + return + } + + var useNative = true + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip_v2"] { + useNative = false + } + var isAd = false + if let contentInfo = item.contentInfo { + switch contentInfo { + case let .message(message, _): + isAd = message.adAttribute != nil + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) + case let .webPage(webPage, media, _): + self.footerContentNode.setWebPage(webPage, media: media) + } + } + if isAd { + useNative = false + } + if let content = item.content as? NativeVideoContent { + if content.fileReference.media.isAnimated { + useNative = false + } + } + if !useNative { + return + } + + var hiddenMedia: (MessageId, Media)? = nil + switch item.contentInfo { + case let .message(message, _): + for media in message.media { + if let media = media as? TelegramMediaImage { + hiddenMedia = (message.id, media) + } else if let media = media as? TelegramMediaFile, media.isVideo { + hiddenMedia = (message.id, media) + } + } + default: + break + } + + if #available(iOS 15.0, *) { + var didExpand = false + let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in + guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else { + return + } + self.activePictureInPictureNavigationController = navigationController + self.activePictureInPictureController = controller + + controller.view.alpha = 0.0 + controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.completeCustomDismiss(true) }) - } + if let videoNode = self.videoNode { + videoNode.setNativePictureInPictureIsActive(false) + } + didExpand = false + }, didBegin: { [weak self] _ in + guard let self else { + return + } + let _ = self + }, didEnd: { [weak self] _ in + guard let self else { + return + } + if let videoNode = self.videoNode { + videoNode.setNativePictureInPictureIsActive(false) + } + + if !didExpand { + self.activePictureInPictureController = nil + self.activePictureInPictureNavigationController = nil + } + }, expand: { [weak self] completion in + didExpand = true + + guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else { + completion() + return + } + + self.activePictureInPictureController = nil + self.activePictureInPictureNavigationController = nil + + activePictureInPictureController.presentationArguments = nil + activePictureInPictureNavigationController.currentWindow?.present(activePictureInPictureController, on: .root, blockInteraction: false, completion: { + }) + + activePictureInPictureController.view.alpha = 1.0 + activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35, completion: { _ in + }) + + completion() + }) + + self.nativePictureInPictureContent = content } } @objc func pictureInPictureButtonPressed() { + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.beginPictureInPicture() + return + } + } + var isNativePictureInPictureSupported = false switch self.item?.contentInfo { case let .message(message, _): @@ -2253,9 +3034,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let playbackRate = self.playbackRate if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported { + self.disablePictureInPicturePlaceholder = true - let overlayVideoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) + let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil) overlayVideoNode.frame = absoluteRect overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate) @@ -2284,7 +3066,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let strongSelf = self else { return } - strongSelf.completeCustomDismiss() + strongSelf.completeCustomDismiss(false) }, expand: { [weak baseNavigationController] completion in guard let contentInfo = item.contentInfo else { return @@ -2338,7 +3120,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { shouldBeDismissed = .single(false) } - let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { + let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) @@ -2417,7 +3199,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.beginCustomDismiss(false) self.statusNode.isHidden = true self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() + self?.completeCustomDismiss(false) }) } } @@ -2485,29 +3267,46 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) } - private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } + var dismissImpl: (() -> Void)? - let items: Signal<[ContextMenuItem], NoError> + let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { - items = self.adMenuMainItems() + items = self.adMenuMainItems() |> map { items in + return (items, []) + } } else { - items = self.contextMenuMainItems(dismiss: { + items = self.contextMenuMainItems(isSettings: isSettings, dismiss: { dismissImpl?() }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - self.isShowingContextMenuPromise.set(true) + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in + if !items.topItems.isEmpty { + return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems)) + } else { + return ContextController.Items(id: AnyHashable(0), content: .list(items.items)) + } + }, gesture: gesture) + if isSettings { + self.isShowingSettingsMenuPromise.set(true) + } else { + self.isShowingContextMenuPromise.set(true) + } controller.presentInGlobalOverlay(contextController) dismissImpl = { [weak contextController] in contextController?.dismiss() } contextController.dismissed = { [weak self] in - Queue.mainQueue().after(0.1, { - self?.isShowingContextMenuPromise.set(false) + Queue.mainQueue().after(isSettings ? 0.0 : 0.1, { + if isSettings { + self?.isShowingSettingsMenuPromise.set(false) + } else { + self?.isShowingContextMenuPromise.set(false) + } }) } } @@ -2537,7 +3336,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }, iconSource: nil, action: { [weak self] _, f in f(.dismissWithoutContent) if let navigationController = self?.baseNavigationController() as? NavigationController { - navigationController.pushViewController(AdsInfoScreen(context: context, forceDark: true)) + navigationController.pushViewController(AdsInfoScreen(context: context, mode: .channel, forceDark: true)) } }))) @@ -2650,9 +3449,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } - private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { + private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> { guard let videoNode = self.videoNode, let item = self.item else { - return .single([]) + return .single(([], [])) } let peer: Signal @@ -2662,222 +3461,334 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { peer = .single(nil) } - return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer) - |> take(1) - |> map { [weak self] status, peer -> [ContextMenuItem] in + return combineLatest(queue: Queue.mainQueue(), + videoNode.status |> take(1), + peer, + videoNode.videoQualityStateSignal() + ) + |> map { [weak self] status, peer, videoQualityState -> (items: [ContextMenuItem], topItems: [ContextMenuItem]) in guard let status = status, let strongSelf = self else { - return [] + return ([], []) } + var topItems: [ContextMenuItem] = [] var items: [ContextMenuItem] = [] - var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal - var speedIconText: String = "1x" - var didSetSpeedValue = false - for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - if abs(speed - status.baseRate) < 0.01 { - speedValue = text - speedIconText = iconText - didSetSpeedValue = true - break - } - } - if !didSetSpeedValue && status.baseRate != 1.0 { - speedValue = String(format: "%.1fx", status.baseRate) - speedIconText = speedValue - } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in - return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) - return - } - - c?.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - - items.append(.separator) - - if let (message, _, _) = strongSelf.contentInfo() { - let context = strongSelf.context - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - guard let strongSelf = self, let peer = peer else { + if isSettings { + let sliderValuePromise = ValuePromise(nil) + topItems.append(.custom(SliderContextItem(title: strongSelf.presentationData.strings.Gallery_VideoSettings_SpeedControlTitle, minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { return } - if let navigationController = strongSelf.baseNavigationController() { - strongSelf.beginCustomDismiss(true) - - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) - - Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss() - } + let newValue = normalizeValue(newValue) + videoNode.setBaseRate(newValue) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(newValue) } - f(.default) - }))) - } - -// if #available(iOS 11.0, *) { -// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in -// f(.default) -// guard let strongSelf = self else { -// return -// } -// strongSelf.beginAirPlaySetup() -// }))) -// } - - if let (message, _, _) = strongSelf.contentInfo() { - for media in message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - let url = content.url - - let item = OpenInItem.url(url: url) - let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn - items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in + sliderValuePromise.set(newValue) + }), true)) + + if let videoQualityState, !videoQualityState.available.isEmpty { + } else { + items.append(.custom(SectionTitleContextItem(text: strongSelf.presentationData.strings.Gallery_VideoSettings_SpeedSectionTitle), false)) + for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { + let isSelected = abs(status.baseRate - rate) < 0.01 + items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() + |> map { value in + if isSelected && value == nil { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }), action: { _, f in f(.default) + + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { + return + } - if let strongSelf = self, let controller = strongSelf.galleryController() { - var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - if !presentationData.theme.overallDarkAppearance { - presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - } - let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in - if let strongSelf = self { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) - } - }) - controller.present(actionSheet, in: .window(.root)) + videoNode.setBaseRate(rate) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) } }))) - break } } - } - - if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - f(.default) + + if let videoQualityState, !videoQualityState.available.isEmpty { + items.append(.custom(SectionTitleContextItem(text: strongSelf.presentationData.strings.Gallery_VideoSettings_QualitySectionTitle), false)) + + do { + let isSelected = videoQualityState.preferred == .auto + let qualityText: String = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityAuto + let textLayout: ContextMenuActionItemTextLayout + if videoQualityState.current != 0 { + textLayout = .secondLineWithValue("\(videoQualityState.current)p") + } else { + textLayout = .singleLine + } + items.append(.action(ContextMenuActionItem(id: AnyHashable("q"), text: qualityText, textLayout: textLayout, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.auto) + self.videoQualityPromise.set(.auto) + }))) + } + + for quality in videoQualityState.available { + let isSelected = videoQualityState.preferred == .quality(quality) + let qualityTitle: String + if quality <= 360 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityLow + } else if quality <= 480 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityMedium + } else if quality <= 720 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityHD + } else if quality <= 1080 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityFHD + } else { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityQHD + } + items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p"), icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.quality(quality)) + self.videoQualityPromise.set(.quality(quality)) - if let strongSelf = self { - switch strongSelf.fetchStatus { - case .Local: - let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { - guard let strongSelf = self else { + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) + } + } + } else { + if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_MenuSaveToGallery, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in + guard let self else { + c?.dismiss(result: .default, completion: nil) + return + } + + if let content = item.content as? HLSVideoContent { + guard let videoNode = self.videoNode, let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else { + return + } + if qualityState.available.isEmpty { + return + } + guard let qualitySet = HLSQualitySet(baseFile: content.fileReference) else { + return + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { c, _ in + c?.popItems() + }))) + + let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in + guard let qualityFileSize = qualityFile.media.size else { return } - guard let controller = strongSelf.galleryController() else { + let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData)) + let title: String + if let quality { + title = self.presentationData.strings.Gallery_SaveToGallery_Quality("\(quality)").string + } else { + title = self.presentationData.strings.Gallery_SaveToGallery_Original + } + items.append(.action(ContextMenuActionItem(text: title, textLayout: .secondLineWithValue(fileSizeString), icon: { _ in + return nil + }, action: { [weak self] c, _ in + c?.dismiss(result: .default, completion: nil) + + guard let self else { + return + } + guard let controller = self.galleryController() else { + return + } + + let saveScreen = SaveProgressScreen(context: self.context, content: .progress(self.presentationData.strings.Story_TooltipSaving, 0.0)) + controller.present(saveScreen, in: .current) + + let stringSaving = self.presentationData.strings.Story_TooltipSaving + let stringSaved = self.presentationData.strings.Story_TooltipSaved + + let saveFileReference: AnyMediaReference = qualityFile.abstract + let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) + + let disposable = (saveSignal + |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in + guard let saveScreen else { + return + } + saveScreen.content = .progress(stringSaving, progress) + }, completed: { [weak saveScreen] in + guard let saveScreen else { + return + } + saveScreen.content = .completion(stringSaved) + Queue.mainQueue().after(3.0, { [weak saveScreen] in + saveScreen?.dismiss() + }) + }) + + saveScreen.cancelled = { + disposable.dispose() + } + }))) + } + + if self.context.isPremium { + addItem(nil, content.fileReference) + } else { + #if DEBUG + addItem(nil, content.fileReference) + #endif + } + + for quality in qualityState.available { + guard let qualityFile = qualitySet.qualityFiles[quality] else { + continue + } + addItem(quality, qualityFile) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) + } else { + c?.dismiss(result: .default, completion: nil) + + switch self.fetchStatus { + case .Local: + let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + guard let controller = self.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: self.presentationData, content: .mediaSaved(text: self.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + default: + guard let controller = self.galleryController() else { return } - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - }) - default: - guard let controller = strongSelf.galleryController() else { - return + controller.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) } - controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) } - } - }))) - } - - if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - if let self, let navigationController = self.baseNavigationController() { - self.beginCustomDismiss(true) - - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) - - Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + if let (message, _, _) = strongSelf.contentInfo() { + let context = strongSelf.context + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss(false) + } + } + f(.default) + }))) + } + + // if #available(iOS 11.0, *) { + // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + // f(.default) + // guard let strongSelf = self else { + // return + // } + // strongSelf.beginAirPlaySetup() + // }))) + // } + + if let (message, _, _) = strongSelf.contentInfo() { + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + let url = content.url + + let item = OpenInItem.url(url: url) + let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn + items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let strongSelf = self, let controller = strongSelf.galleryController() { + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in + if let strongSelf = self { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) + } + }) + controller.present(actionSheet, in: .window(.root)) + } + }))) + break } } - f(.default) - }))) - } - - if strongSelf.canDelete() { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - f(.default) - - if let strongSelf = self { - strongSelf.footerContentNode.deleteButtonPressed() - } - }))) - } - - return items - } - } - - private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { - guard let videoNode = self.videoNode else { - return .single([]) - } - - return videoNode.status - |> take(1) - |> deliverOnMainQueue - |> map { [weak self] status -> [ContextMenuItem] in - guard let status = status, let strongSelf = self else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) - return } - c?.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - - let sliderValuePromise = ValuePromise(nil) - items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return + + if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + if let self, let navigationController = self.baseNavigationController() { + self.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) + + Queue.mainQueue().after(0.3) { + self.completeCustomDismiss(false) + } + } + f(.default) + }))) } - let newValue = normalizeValue(newValue) - videoNode.setBaseRate(newValue) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(newValue) + + if strongSelf.canDelete() { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + f(.default) + + if let strongSelf = self { + strongSelf.footerContentNode.deleteButtonPressed() + } + }))) } - sliderValuePromise.set(newValue) - }), true)) - - items.append(.separator) - - for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - let isSelected = abs(status.baseRate - rate) < 0.01 - items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() - |> map { value in - if isSelected && value == nil { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }), action: { _, f in - f(.default) - - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return - } - - videoNode.setBaseRate(rate) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - } - }))) } - return items + return (items, topItems) } } @@ -2963,6 +3874,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } + @objc private func settingsButtonPressed() { + self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true) + } + override func adjustForPreviewing() { super.adjustForPreviewing() @@ -2983,6 +3898,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.playbackRatePromise.set(self.playbackRate ?? 1.0) } + func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.videoQuality = videoQuality + self.videoQualityPromise.set(videoQuality) + + self.videoNode?.setVideoQuality(videoQuality) + } + public func seekToStart() { self.videoNode?.seek(0.0) self.videoNode?.play() @@ -3059,6 +3981,59 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return keyShortcuts } + + override func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + if case .right = edge { + return true + } else { + return false + } + } + + override func setActiveEdgeAction(edge: ActiveEdge?) { + guard let videoNode = self.videoNode else { + return + } + if let edge, case .right = edge { + let effectiveRate: Double + if let current = self.activeEdgeRateState { + effectiveRate = min(2.5, current.initialRate + 0.5) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + } else { + guard let playbackRate = self.playbackRate else { + return + } + effectiveRate = min(2.5, playbackRate + 0.5) + self.activeEdgeRateState = (playbackRate, effectiveRate) + } + videoNode.setBaseRate(effectiveRate) + } else if let (initialRate, _) = self.activeEdgeRateState { + self.activeEdgeRateState = nil + videoNode.setBaseRate(initialRate) + } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + + override func adjustActiveEdgeAction(distance: CGFloat) { + guard let videoNode = self.videoNode else { + return + } + if let current = self.activeEdgeRateState { + var rateFraction = Double(distance) / 100.0 + rateFraction = max(0.0, min(1.0, rateFraction)) + let rateDistance = (current.initialRate + 0.5) * (1.0 - rateFraction) + 2.5 * rateFraction + let effectiveRate = max(1.0, min(2.5, rateDistance)) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + videoNode.setBaseRate(effectiveRate) + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + } } final class HeaderContextReferenceContentSource: ContextReferenceContentSource { diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 0360571fe4e..2d5c388ef58 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -300,7 +300,7 @@ public final class SecretMediaPreviewController: ViewController { } } - self.controllerNode.completeCustomDismiss = { [weak self] in + self.controllerNode.completeCustomDismiss = { [weak self] _ in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift index fbcf016de4d..f07d8281445 100644 --- a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift @@ -59,12 +59,19 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, ASScrollViewDelegate self.addSubnode(self.scrollNode) } + open func contentTapAction() -> Bool { + return false + } + @objc open func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if recognizer.state == .ended { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { let pointInNode = self.scrollNode.view.convert(location, to: self.view) if pointInNode.x < 44.0 || pointInNode.x > self.frame.width - 44.0 { } else { + if self.contentTapAction() { + return + } switch gesture { case .tap: self.toggleControlsVisibility() diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift index 8d9774fa20c..1b865e41eaf 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -342,13 +342,16 @@ class GeneralChartComponentController: ChartThemeContainer { visible: chartVisibility[index]) } - if let currency, let firstValue = values.first, let color = GColor(hexString: "#dda747") { + if let currency, let firstValue = values.first, let starColor = GColor(hexString: "#dda747") { let updatedTitle: String + let color: GColor switch currency { case .ton: updatedTitle = self.strings.revenueInTon + color = firstValue.color case .xtr: updatedTitle = self.strings.revenueInStars + color = starColor } values[0] = ChartDetailsViewModel.Value( prefix: nil, diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 9c8735eeba4..e9dced293f0 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -28,6 +28,10 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index dfdd9fbaef8..811950283c1 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -12,13 +12,21 @@ import AnimationCache import MultiAnimationRenderer public final class HashtagSearchController: TelegramBaseController { + public enum Mode: Equatable { + case generic + case noChat + case chatOnly + } + private let queue = Queue() private let context: AccountContext private let peer: EnginePeer? private let query: String - let all: Bool + let mode: Mode let publicPosts: Bool + let stories: Bool + let forceDark: Bool private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() @@ -33,23 +41,34 @@ public final class HashtagSearchController: TelegramBaseController { return self.displayNode as! HashtagSearchControllerNode } - public init(context: AccountContext, peer: EnginePeer?, query: String, all: Bool = false, publicPosts: Bool = false) { + public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false, stories: Bool = false, forceDark: Bool = false) { self.context = context self.peer = peer self.query = query - self.all = all + self.mode = mode self.publicPosts = publicPosts + self.stories = stories + self.forceDark = forceDark self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.title = query + var title = query + if case .chatOnly = mode, let addressName = peer?.addressName { + title = "\(query)@\(addressName)" + } + + self.title = title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.presentationDataDisposable = (self.context.sharedContext.presentationData @@ -58,6 +77,11 @@ public final class HashtagSearchController: TelegramBaseController { let previousTheme = self.presentationData.theme let previousStrings = self.presentationData.strings + var presentationData = presentationData + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 3ef278aa27f..2da8cbbe44c 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -9,10 +9,13 @@ import AccountContext import ChatListUI import SegmentedControlNode import ChatListSearchItemHeader +import PeerInfoVisualMediaPaneNode +import UIKitRuntimeUtils final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private let context: AccountContext private weak var controller: HashtagSearchController? + private let peer: EnginePeer? private var query: String private var isCashtag = false private var presentationData: PresentationData @@ -29,6 +32,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg private let isSearching = Promise() private var isSearchingDisposable: Disposable? + private var searchResultsCount: Int32 = 0 + private var searchResultsCountDisposable: Disposable? + private let clippingNode: ASDisplayNode private let containerNode: ASDisplayNode let currentController: ChatController? @@ -38,10 +44,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalController: ChatController? let globalChatContents: HashtagSearchGlobalChatContents? - private var globalStorySearchContext: SearchStoryListContext? - private var globalStorySearchDisposable = MetaDisposable() - private var globalStorySearchState: StoryListContext.State? - private var globalStorySearchComponentView: ComponentView? + private var storySearchContext: SearchStoryListContext? + private var storySearchDisposable = MetaDisposable() + private var storySearchState: StoryListContext.State? + private var storySearchComponentView: ComponentView? + + private var storySearchPaneNode: PeerInfoStoryPaneNode? + private var isDisplayingStories = false private var panRecognizer: InteractiveTransitionGestureRecognizer? @@ -51,19 +60,26 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg init(context: AccountContext, controller: HashtagSearchController, peer: EnginePeer?, query: String, navigationBar: NavigationBar?, navigationController: NavigationController?) { self.context = context self.controller = controller + self.peer = peer self.query = query self.navigationBar = navigationBar self.isCashtag = query.hasPrefix("$") self.presentationData = controller.presentationData + self.isDisplayingStories = controller.stories - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + var controllerParams: ChatControllerParams? + if controller.forceDark { + controllerParams = ChatControllerParams(forcedTheme: defaultDarkColorPresentationTheme, forcedWallpaper: defaultBuiltinWallpaper(data: .default, colors: defaultDarkWallpaperGradientColors.map(\.rgb), intensity: -34)) + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.containerNode = ASDisplayNode() - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, hasTabs: controller.mode != .chatOnly, cancel: { [weak controller] in controller?.dismiss() }) @@ -75,8 +91,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.recentListNode.alpha = 0.0 let navigationController = controller.navigationController as? NavigationController - if let peer, !controller.all { - self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil) + if let peer, controller.mode != .noChat { + self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: controllerParams) self.currentController?.alwaysShowSearchResultsAsList = true self.currentController?.showListEmptyResults = true self.currentController?.customNavigationController = navigationController @@ -84,16 +100,21 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController = nil } - let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) - self.myChatContents = myChatContents - self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: nil) - self.myController?.alwaysShowSearchResultsAsList = true - self.myController?.showListEmptyResults = true - self.myController?.customNavigationController = navigationController + if let _ = peer, controller.mode != .chatOnly { + let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) + self.myChatContents = myChatContents + self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) + self.myController?.alwaysShowSearchResultsAsList = true + self.myController?.showListEmptyResults = true + self.myController?.customNavigationController = navigationController + } else { + self.myChatContents = nil + self.myController = nil + } let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true) self.globalChatContents = globalChatContents - self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: nil) + self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) self.globalController?.alwaysShowSearchResultsAsList = true self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController @@ -117,7 +138,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.containerNode) - if controller.all { + if controller.mode == .noChat { self.isSearching.set(self.myChatContents?.searching ?? .single(false)) } else { if let _ = peer { @@ -171,9 +192,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } - navigationBar?.setContentNode(self.searchContentNode, animated: false) + if controller.mode != .chatOnly { + navigationBar?.setContentNode(self.searchContentNode, animated: false) + } - self.addSubnode(self.shimmerNode) + if !self.isDisplayingStories { + self.addSubnode(self.shimmerNode) + } self.searchContentNode.setQueryUpdated { [weak self] query in self?.searchQueryPromise.set(query) @@ -218,13 +243,25 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } }) + if let currentController = self.currentController { + self.searchResultsCountDisposable = (currentController.searchResultsCount.get() + |> deliverOnMainQueue).start(next: { [weak self] searchResultsCount in + guard let self else { + return + } + self.searchResultsCount = searchResultsCount + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + }) + } + self.updateStorySearch() } deinit { self.searchQueryDisposable?.dispose() self.isSearchingDisposable?.dispose() - self.globalStorySearchDisposable.dispose() + self.searchResultsCountDisposable?.dispose() + self.storySearchDisposable.dispose() } private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections { @@ -364,25 +401,30 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } private func updateStorySearch() { - self.globalStorySearchState = nil - self.globalStorySearchDisposable.set(nil) - self.globalStorySearchContext = nil + self.storySearchState = nil + self.storySearchDisposable.set(nil) + self.storySearchContext = nil if !self.query.isEmpty { - let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(self.query)) - self.globalStorySearchDisposable.set((globalStorySearchContext.state + var peerId: EnginePeer.Id? + if self.controller?.mode == .chatOnly { + peerId = self.peer?.id + } + let storySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query)) + self.storySearchDisposable.set((storySearchContext.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } if state.totalCount > 0 { - self.globalStorySearchState = state + self.storySearchState = state } else { - self.globalStorySearchState = nil + self.storySearchState = nil + self.currentController?.externalSearchResultsCount = nil } - self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) })) - self.globalStorySearchContext = globalStorySearchContext + self.storySearchContext = storySearchContext } } @@ -407,6 +449,37 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + private func animateContentOut() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.3, removeOnCompletion: false) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(30.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 0.0 as NSNumber, to: 30.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false) + } + } + + private func animateContentIn() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 30.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak controller] completed in + guard let controller, completed else { + return + } + controller.contentContainerNode.layer.filters = [] + }) + } + } + func requestUpdate(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.containerLayout { let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) @@ -427,59 +500,69 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.insertSubnode(self.clippingNode, at: 0) } - if let controller = self.currentController { - transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) - controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 79.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) - - if controller.displayNode.supernode == nil { - controller.viewWillAppear(false) - self.containerNode.addSubnode(controller.displayNode) - controller.viewDidAppear(false) - - controller.beginMessageSearch(self.query) + var storyParentController: ViewController? + if self.controller?.mode == .chatOnly { + storyParentController = self.currentController + } else { + storyParentController = self.globalController + } + + var currentTopInset: CGFloat = 0.0 + var globalTopInset: CGFloat = 0.0 + + var panelSearchState: StoryResultsPanelComponent.SearchState? + if let storySearchState = self.storySearchState { + if self.isDisplayingStories { + if self.searchResultsCount > 0 { + panelSearchState = .messages(self.searchResultsCount) + } + } else { + panelSearchState = .stories(storySearchState) } } - if let controller = self.myController { - transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width, y: 0.0), size: layout.size)) - controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) - - if controller.displayNode.supernode == nil { - controller.viewWillAppear(false) - self.containerNode.addSubnode(controller.displayNode) - controller.viewDidAppear(false) - - controller.beginMessageSearch(self.query) + if self.isDisplayingStories { + if let storySearchState = self.storySearchState { + self.currentController?.externalSearchResultsCount = Int32(storySearchState.totalCount) + } else { + self.currentController?.externalSearchResultsCount = nil } + } else { + self.currentController?.externalSearchResultsCount = nil } - if let controller = self.globalController { - var topInset: CGFloat = insets.top - 89.0 - if let state = self.globalStorySearchState { + if let panelSearchState { + if let storyParentController { let componentView: ComponentView var panelTransition = ComponentTransition(transition) - if let current = self.globalStorySearchComponentView { + if let current = self.storySearchComponentView { componentView = current } else { panelTransition = .immediate componentView = ComponentView() - self.globalStorySearchComponentView = componentView + self.storySearchComponentView = componentView } let panelSize = componentView.update( - transition: .immediate, + transition: panelTransition, component: AnyComponent(StoryResultsPanelComponent( context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, query: self.query, - state: state, + peer: self.controller?.mode == .chatOnly ? self.peer : nil, + state: panelSearchState, sideInset: layout.safeInsets.left, action: { [weak self] in guard let self else { return } - let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(self.query), listContext: self.globalStorySearchContext) - self.controller?.push(searchController) + if self.controller?.mode == .chatOnly { + self.isDisplayingStories = !self.isDisplayingStories + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } else { + let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(nil, self.query), listContext: self.storySearchContext) + self.controller?.push(searchController) + } } )), environment: {}, @@ -488,16 +571,54 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) if let view = componentView.view { if view.superview == nil { - controller.view.addSubview(view) - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true) + storyParentController.view.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } panelTransition.setFrame(view: view, frame: panelFrame) } - topInset += panelSize.height - } else if let globalStorySearchComponentView = self.globalStorySearchComponentView { - globalStorySearchComponentView.view?.removeFromSuperview() - self.globalStorySearchComponentView = nil + if self.controller?.mode == .chatOnly { + currentTopInset += panelSize.height + } else { + globalTopInset += panelSize.height + } } + } else if let storySearchComponentView = self.storySearchComponentView { + storySearchComponentView.view?.removeFromSuperview() + self.storySearchComponentView = nil + } + + if let controller = self.currentController { + var topInset: CGFloat = insets.top - 79.0 + topInset += currentTopInset + + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + + if controller.displayNode.supernode == nil { + controller.viewWillAppear(false) + self.containerNode.addSubnode(controller.displayNode) + controller.viewDidAppear(false) + + controller.beginMessageSearch(self.query) + } + } + + if let controller = self.myController { + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width, y: 0.0), size: layout.size)) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + + if controller.displayNode.supernode == nil { + controller.viewWillAppear(false) + self.containerNode.addSubnode(controller.displayNode) + controller.viewDidAppear(false) + + controller.beginMessageSearch(self.query) + } + } + + if let controller = self.globalController { + var topInset: CGFloat = insets.top - 89.0 + topInset += globalTopInset transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width * 2.0, y: 0.0), size: layout.size)) controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) @@ -511,6 +632,75 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + if self.isDisplayingStories, let peer = self.peer, let storySearchContext = self.storySearchContext { + let storySearchPaneNode: PeerInfoStoryPaneNode + var paneTransition = transition + if let current = self.storySearchPaneNode { + storySearchPaneNode = current + } else { + storySearchPaneNode = PeerInfoStoryPaneNode( + context: self.context, + scope: .search(peerId: peer.id, query: self.query), + captureProtected: false, + isProfileEmbedded: false, + canManageStories: false, + navigationController: { [weak self] in + guard let self else { + return nil + } + return self.controller?.navigationController as? NavigationController + }, + listContext: storySearchContext + ) + self.storySearchPaneNode = storySearchPaneNode + if let storySearchView = self.storySearchComponentView?.view { + storySearchView.superview?.insertSubview(storySearchPaneNode.view, belowSubview: storySearchView) + } else { + storyParentController?.view.addSubview(storySearchPaneNode.view) + } + paneTransition = .immediate + + if transition.isAnimated { + storySearchPaneNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + storySearchPaneNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentOut() + } + } + + var bottomInset: CGFloat = 0.0 + if case .regular = layout.metrics.widthClass { + bottomInset += 49.0 + } else { + bottomInset += 45.0 + } + bottomInset += layout.intrinsicInsets.bottom + + storySearchPaneNode.update( + size: layout.size, + topInset: navigationBarHeight, + sideInset: layout.safeInsets.left, + bottomInset: 0.0, + deviceMetrics: layout.deviceMetrics, + visibleHeight: layout.size.height - currentTopInset, + isScrollingLockedAtTop: false, + expandProgress: 1.0, + navigationHeight: 0.0, + presentationData: self.presentationData, + synchronous: false, + transition: paneTransition + ) + storySearchPaneNode.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + paneTransition.updateFrame(node: storySearchPaneNode, frame: CGRect(origin: CGPoint(x: 0.0, y: currentTopInset), size: CGSize(width: layout.size.width, height: layout.size.height - bottomInset - currentTopInset))) + } else if let storySearchPaneNode = self.storySearchPaneNode { + self.storySearchPaneNode = nil + + storySearchPaneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + storySearchPaneNode.view.removeFromSuperview() + }) + storySearchPaneNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentIn() + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: .zero, size: layout.size)) let containerPosition: CGFloat = -layout.size.width * CGFloat(self.searchContentNode.selectedIndex) - self.panTransitionFraction * layout.size.width @@ -522,7 +712,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.shimmerNode.update(context: self.context, size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height), presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, key: .chats, hasSelection: false, transition: transition) if isFirstTime { - self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + if self.shimmerNode.supernode != nil { + self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + } else { + self.insertSubnode(self.recentListNode, aboveSubnode: self.clippingNode) + } } transition.updateFrame(node: self.recentListNode, frame: CGRect(origin: .zero, size: layout.size)) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index 33eb723ab76..d197166f392 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -16,6 +16,7 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { private var theme: PresentationTheme private let strings: PresentationStrings private let hasCurrentChat: Bool + private let hasTabs: Bool private let cancel: () -> Void @@ -60,10 +61,11 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, hasCurrentChat: Bool, cancel: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, hasCurrentChat: Bool, hasTabs: Bool, cancel: @escaping () -> Void) { self.theme = theme self.strings = strings self.hasCurrentChat = hasCurrentChat + self.hasTabs = hasTabs self.cancel = cancel @@ -135,50 +137,47 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.frame = searchBarFrame self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset + sideInset, rightInset: rightInset + sideInset, transition: transition) - var items: [TabSelectorComponent.Item] = [] - if self.hasCurrentChat { - items.append(TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat)) - } - items.append(TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages)) - items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) - - let tabSelectorSize = self.tabSelector.update( - transition: ComponentTransition(transition), - component: AnyComponent(TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: self.theme.list.itemSecondaryTextColor, - selection: self.theme.list.itemAccentColor - ), - customLayout: TabSelectorComponent.CustomLayout( - font: Font.medium(14.0), - spacing: self.hasCurrentChat ? 24.0 : 8.0, - lineSelection: true - ), - items: items, - selectedId: AnyHashable(self.selectedIndex), - setSelectedId: { [weak self] id in - guard let self, let index = id.base as? Int else { - return - } - self.indexUpdated?(index) - }, - transitionFraction: self.transitionFraction - )), - environment: {}, - containerSize: CGSize(width: size.width, height: 44.0) - ) - let tabSelectorFrameOriginX: CGFloat - if self.hasCurrentChat || "".isEmpty { - tabSelectorFrameOriginX = floorToScreenPixels((size.width - tabSelectorSize.width) / 2.0) - } else { - tabSelectorFrameOriginX = 4.0 - } - let tabSelectorFrame = CGRect(origin: CGPoint(x: tabSelectorFrameOriginX, y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) - if let tabSelectorView = self.tabSelector.view { - if tabSelectorView.superview == nil { - self.view.addSubview(tabSelectorView) + if self.hasTabs { + var items: [TabSelectorComponent.Item] = [] + if self.hasCurrentChat { + items.append(TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat)) + } + items.append(TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages)) + items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) + + let tabSelectorSize = self.tabSelector.update( + transition: ComponentTransition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.theme.list.itemSecondaryTextColor, + selection: self.theme.list.itemAccentColor + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: self.hasCurrentChat ? 24.0 : 8.0, + lineSelection: true + ), + items: items, + selectedId: AnyHashable(self.selectedIndex), + setSelectedId: { [weak self] id in + guard let self, let index = id.base as? Int else { + return + } + self.indexUpdated?(index) + }, + transitionFraction: self.transitionFraction + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + let tabSelectorFrameOriginX = floorToScreenPixels((size.width - tabSelectorSize.width) / 2.0) + let tabSelectorFrame = CGRect(origin: CGPoint(x: tabSelectorFrameOriginX, y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) + if let tabSelectorView = self.tabSelector.view { + if tabSelectorView.superview == nil { + self.view.addSubview(tabSelectorView) + } + transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) } - transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) } } diff --git a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift index 32a693623c6..6ee748f476c 100644 --- a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift +++ b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift @@ -7,13 +7,20 @@ import MultilineTextComponent import BundleIconComponent import StorySetIndicatorComponent import AccountContext +import AnimatedTextComponent +import BlurredBackgroundComponent final class StoryResultsPanelComponent: CombinedComponent { + enum SearchState: Equatable { + case stories(StoryListContext.State) + case messages(Int32) + } let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let query: String - let state: StoryListContext.State + let peer: EnginePeer? + let state: SearchState let sideInset: CGFloat let action: () -> Void @@ -22,7 +29,8 @@ final class StoryResultsPanelComponent: CombinedComponent { theme: PresentationTheme, strings: PresentationStrings, query: String, - state: StoryListContext.State, + peer: EnginePeer?, + state: SearchState, sideInset: CGFloat, action: @escaping () -> Void ) { @@ -30,6 +38,7 @@ final class StoryResultsPanelComponent: CombinedComponent { self.theme = theme self.strings = strings self.query = query + self.peer = peer self.state = state self.sideInset = sideInset self.action = action @@ -45,6 +54,9 @@ final class StoryResultsPanelComponent: CombinedComponent { if lhs.query != rhs.query { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.state != rhs.state { return false } @@ -55,10 +67,11 @@ final class StoryResultsPanelComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) + let background = Child(BlurredBackgroundComponent.self) let avatars = Child(StorySetIndicatorComponent.self) + let titlePrefix = Child(AnimatedTextComponent.self) let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) + let text = Child(AnimatedTextComponent.self) let arrow = Child(BundleIconComponent.self) let separator = Child(Rectangle.self) let button = Child(Button.self) @@ -68,63 +81,119 @@ final class StoryResultsPanelComponent: CombinedComponent { let spacing: CGFloat = 3.0 - let textLeftInset: CGFloat = 81.0 + component.sideInset + var textLeftInset: CGFloat = 16.0 + component.sideInset let textTopInset: CGFloat = 9.0 var existingPeerIds = Set() var items: [StorySetIndicatorComponent.Item] = [] - for item in component.state.items { - guard let peer = item.peer, !existingPeerIds.contains(peer.id) else { - continue + switch component.state { + case let .stories(state): + for item in state.items { + guard let peer = item.peer, !existingPeerIds.contains(peer.id) || component.peer != nil else { + continue + } + existingPeerIds.insert(peer.id) + items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) } - existingPeerIds.insert(peer.id) - items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) + textLeftInset += 65.0 + default: + break + } + + var titlePrefixString: [AnimatedTextComponent.Item] = [] + let titleString: NSAttributedString + var textString: [AnimatedTextComponent.Item] = [] + if let peer = component.peer, let username = peer.addressName { + let entityType: String + switch component.state { + case let .messages(count): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Posts(count)) + )] + entityType = component.strings.HashtagSearch_FoundPosts + case let .stories(state): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Stories(Int32(state.totalCount))) + )] + entityType = component.strings.HashtagSearch_FoundStories + } + let fullString = component.strings.HashtagSearch_LocalStoriesFound("", "@\(username)") + titleString = NSMutableAttributedString( + string: fullString.string, + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + ) + if let lastRange = fullString.ranges.last?.range { + (titleString as? NSMutableAttributedString)?.addAttribute(NSAttributedString.Key.foregroundColor, value: component.theme.rootController.navigationBar.accentTextColor, range: lastRange) + } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(entityType), + 1: .text(component.query) + ]) + } else { + if case let .stories(state) = component.state { + titleString = NSAttributedString( + string: component.strings.HashtagSearch_StoriesFound(Int32(state.totalCount)), + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + ) + } else { + titleString = NSAttributedString() + } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(component.strings.HashtagSearch_FoundStories), + 1: .text(component.query) + ]) } - let avatars = avatars.update( - component: StorySetIndicatorComponent( - context: component.context, - strings: component.strings, - items: Array(items.prefix(3)), - displayAvatars: true, - hasUnseen: true, - hasUnseenPrivate: false, - totalCount: 0, - theme: component.theme, - action: {} - ), - availableSize: context.availableSize, - transition: .immediate - ) + var titlePrefixOffset: CGFloat = 0.0 + var titlePrefixChild: _UpdatedChildComponent? + if !titlePrefixString.isEmpty { + let titlePrefix = titlePrefix.update( + component: AnimatedTextComponent( + font: Font.semibold(15.0), + color: component.theme.rootController.navigationBar.primaryTextColor, + items: titlePrefixString, + noDelay: true + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0, height: context.availableSize.height), + transition: context.transition + ) + titlePrefixOffset = titlePrefix.size.width + 1.0 + titlePrefixChild = titlePrefix + } let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)), - font: Font.semibold(15.0), - textColor: component.theme.rootController.navigationBar.primaryTextColor, - paragraphAlignment: .natural - )), + text: .plain(titleString), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), - availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0 - titlePrefixOffset, height: CGFloat.greatestFiniteMagnitude), + transition: context.transition ) - + let text = text.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string, - font: Font.regular(14.0), - textColor: component.theme.rootController.navigationBar.secondaryTextColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .natural, - maximumNumberOfLines: 1 + component: AnimatedTextComponent( + font: Font.regular(14.0), + color: component.theme.rootController.navigationBar.secondaryTextColor, + items: textString, + noDelay: true ), availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height), - transition: .immediate + transition: context.transition ) let arrow = arrow.update( @@ -139,7 +208,7 @@ final class StoryResultsPanelComponent: CombinedComponent { let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0) let background = background.update( - component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor), + component: BlurredBackgroundComponent(color: component.theme.rootController.navigationBar.blurredBackgroundColor), availableSize: size, transition: .immediate ) @@ -167,12 +236,37 @@ final class StoryResultsPanelComponent: CombinedComponent { .position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0)) ) - context.add(avatars - .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) - ) + if !items.isEmpty { + let avatars = avatars.update( + component: StorySetIndicatorComponent( + context: component.context, + strings: component.strings, + items: Array(items.prefix(3)), + displayAvatars: component.peer == nil, + hasUnseen: true, + hasUnseenPrivate: false, + totalCount: 0, + theme: component.theme, + action: {} + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(avatars + .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + if let titlePrefixChild { + context.add(titlePrefixChild + .position(CGPoint(x: textLeftInset + titlePrefixChild.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + } + context.add(title - .position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + .position(CGPoint(x: textLeftInset + titlePrefixOffset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text diff --git a/submodules/ICloudResources/Sources/ICloudResources.swift b/submodules/ICloudResources/Sources/ICloudResources.swift index c24cb17a462..2ef67e5e9d9 100644 --- a/submodules/ICloudResources/Sources/ICloudResources.swift +++ b/submodules/ICloudResources/Sources/ICloudResources.swift @@ -132,20 +132,24 @@ public func iCloudFileDescription(_ url: URL) -> Signal Signal Void)? let style: ItemListStyle + let textSize: ItemListTextItemTextSize + let textAlignment: ItemListTextItemTextAlignment let trimBottomInset: Bool + let additionalInsets: UIEdgeInsets public let isAlwaysPlain: Bool = true public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, trimBottomInset: Bool = false) { + public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, textSize: ItemListTextItemTextSize = .generic, textAlignment: ItemListTextItemTextAlignment = .natural, tag: ItemListItemTag? = nil, trimBottomInset: Bool = false, additionalInsets: UIEdgeInsets = .zero) { self.presentationData = presentationData self.text = text self.sectionId = sectionId self.linkAction = linkAction self.style = style + self.textSize = textSize + self.textAlignment = textAlignment self.trimBottomInset = trimBottomInset + self.additionalInsets = additionalInsets self.tag = tag } @@ -127,13 +161,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { return { [weak self] item, params, neighbors in let leftInset: CGFloat = 15.0 - let topInset: CGFloat = 7.0 + var topInset: CGFloat = 7.0 var bottomInset: CGFloat = 7.0 - let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) - let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize)) + var titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + var textColor: UIColor = item.presentationData.theme.list.freeTextColor + if case .large = item.text { + titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize)) + } else if case .larger = item.textSize { + titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize / 17.0 * 15.0)) + textColor = item.presentationData.theme.list.itemSecondaryTextColor + } let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize) - + var themeUpdated = false var chevronImage = currentChevronImage if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -145,14 +185,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { case let .plain(text): attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor) case let .large(text): - attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) case let .markdown(text): - let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) - })).mutableCopy() as! NSMutableAttributedString + }), textAlignment: item.textAlignment.textAlignment).mutableCopy() as! NSMutableAttributedString if let _ = text.range(of: ">]"), let range = mutableAttributedText.string.range(of: ">") { if themeUpdated || currentChevronImage == nil { - chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) + switch item.textSize { + case .generic: + chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) + case .larger: + chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: item.presentationData.theme.list.itemAccentColor) + } } if let chevronImage { mutableAttributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mutableAttributedText.string)) @@ -162,7 +207,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { case let .custom(_, string): attributedText = string } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: item.textAlignment.textAlignment, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize @@ -174,6 +219,10 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { default: break } + + topInset += item.additionalInsets.top + bottomInset += item.additionalInsets.bottom + contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) if item.trimBottomInset { @@ -202,7 +251,15 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } let _ = titleApply(textArguments) - strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) + let titleOrigin: CGFloat + switch item.textAlignment { + case .natural: + titleOrigin = leftInset + params.leftInset + case .center: + titleOrigin = floorToScreenPixels((contentSize.width - titleLayout.size.width) / 2.0) + } + + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: topInset), size: titleLayout.size) } }) } @@ -261,7 +318,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } } - if let rects = rects { + if var rects { let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current @@ -270,6 +327,10 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } + if item.text.text.contains(">]"), var lastRect = rects.last { + lastRect.size.width += 8.0 + rects[rects.count - 1] = lastRect + } linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index fde42317a17..801468bc2cb 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -85,7 +85,7 @@ public final class JoinLinkPreviewController: ViewController { strongSelf.controllerNode.setRequestPeer(image: invite.photoRepresentation, title: invite.title, about: invite.about, memberCount: invite.participantsCount, isGroup: !invite.flags.isBroadcast, isVerified: invite.flags.isVerified, isFake: invite.flags.isFake, isScam: invite.flags.isScam) } else { let data = JoinLinkPreviewData(isGroup: !invite.flags.isBroadcast, isJoined: false) - strongSelf.controllerNode.setInvitePeer(image: invite.photoRepresentation, title: invite.title, memberCount: invite.participantsCount, members: invite.participants?.map({ $0 }) ?? [], data: data) + strongSelf.controllerNode.setInvitePeer(image: invite.photoRepresentation, title: invite.title, about: invite.about, memberCount: invite.participantsCount, members: invite.participants?.map({ $0 }) ?? [], data: data) } case let .alreadyJoined(peer): strongSelf.navigateToPeer(peer, nil) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index 072a48a65d2..7516769aef8 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -392,8 +392,8 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi self.setNeedsLayout() } - func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { - let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, memberCount: memberCount, members: members)) + func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { + let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, about: about, memberCount: memberCount, members: members)) contentNode.join = { [weak self] in self?.join?() } diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 60b8ea8e4c6..6acb4bc2325 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -32,33 +32,33 @@ private final class MoreNode: ASDisplayNode { final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { enum Content { - case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer]) + case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer]) case request(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, isVerified: Bool, isFake: Bool, isScam: Bool) var isGroup: Bool { switch self { - case let .invite(isGroup, _, _, _, _), let .request(isGroup, _, _, _, _, _, _, _): + case let .invite(isGroup, _, _, _, _, _), let .request(isGroup, _, _, _, _, _, _, _): return isGroup } } var image: TelegramMediaImageRepresentation? { switch self { - case let .invite(_, image, _, _, _), let .request(_, image, _, _, _, _, _, _): + case let .invite(_, image, _, _, _, _), let .request(_, image, _, _, _, _, _, _): return image } } var title: String { switch self { - case let .invite(_, _, title, _, _), let .request(_, _, title, _, _, _, _, _): + case let .invite(_, _, title, _, _, _), let .request(_, _, title, _, _, _, _, _): return title } } var memberCount: Int32 { switch self { - case let .invite(_, _, _, memberCount, _), let .request(_, _, _, _, memberCount, _, _, _): + case let .invite(_, _, _, _, memberCount, _), let .request(_, _, _, _, memberCount, _, _, _): return memberCount } } @@ -138,7 +138,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) - if case let .invite(isGroup, _, _, memberCount, members) = content { + if case let .invite(isGroup, _, _, _, memberCount, members) = content { self.peerNodes = members.compactMap { peer in guard peer.id != context.account.peerId else { return nil @@ -176,7 +176,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.addSubnode(self.countNode) let membersString: String if content.isGroup { - if case let .invite(_, _, _, memberCount, members) = content, !members.isEmpty { + if case let .invite(_, _, _, _, memberCount, members) = content, !members.isEmpty { membersString = strings.Invitation_Members(memberCount) } else { membersString = strings.Conversation_StatusMembers(content.memberCount) @@ -195,7 +195,13 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } self.moreNode.flatMap(self.peersScrollNode.addSubnode) - if case let .request(isGroup, _, _, about, _, _, _, _) = content { + switch content { + case let .invite(_, _, _, about, _, _): + if let about = about, !about.isEmpty { + self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + self.addSubnode(self.aboutNode) + } + case let .request(isGroup, _, _, about, _, _, _, _): if let about = about, !about.isEmpty { self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.addSubnode(self.aboutNode) @@ -339,12 +345,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0), size: countSize)) var verticalOffset = verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 18.0 - - if let aboutSize = aboutSize { - transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) - verticalOffset += aboutSize.height + 20.0 - } - + let peerSize = CGSize(width: 85.0, height: 95.0) let peerInset: CGFloat = 10.0 @@ -367,6 +368,11 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer verticalOffset += 100.0 } + if let aboutSize = aboutSize { + transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) + verticalOffset += aboutSize.height + 20.0 + } + let buttonInset: CGFloat = 16.0 let actionButtonHeight = self.actionButtonNode.updateLayout(width: size.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.actionButtonNode, frame: CGRect(x: buttonInset, y: verticalOffset, width: size.width, height: actionButtonHeight)) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index 1f41d51c92e..f4c3addf669 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -403,6 +403,7 @@ - (void)presentPhotoEditorForItem:(id)item tab:(TGP TGPhotoEditorControllerIntent intent = isVideo ? TGPhotoEditorControllerVideoIntent : TGPhotoEditorControllerGenericIntent; TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:editableMediaItem intent:intent adjustments:adjustments caption:caption screenImage:screenImage availableTabs:_interfaceView.currentTabs selectedTab:tab]; + controller.modalPresentationStyle = UIModalPresentationFullScreen; controller.entitiesView = entitiesView; controller.editingContext = _editingContext; controller.stickersContext = _stickersContext; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m index ec3e9d28881..02bfd423baa 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m @@ -26,7 +26,11 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() if ([UIScreen mainScreen].scale >= 2.0f - FLT_EPSILON) { - if (widescreenWidth >= 932.0f - FLT_EPSILON) + if (widescreenWidth >= 956.0f - FLT_EPSILON) + { + return CGSizeMake(145.0f + TGScreenPixel, 145.0 + TGScreenPixel); + } + else if (widescreenWidth >= 932.0f - FLT_EPSILON) { return CGSizeMake(141.0f + TGScreenPixel, 141.0 + TGScreenPixel); } @@ -38,6 +42,10 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() { return CGSizeMake(137.0f - TGScreenPixel, 137.0f - TGScreenPixel); } + else if (widescreenWidth >= 874.0f - FLT_EPSILON) + { + return CGSizeMake(133.0f - TGScreenPixel, 133.0f - TGScreenPixel); + } else if (widescreenWidth >= 852.0f - FLT_EPSILON) { return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel); diff --git a/submodules/LegacyComponents/Sources/TGViewController.mm b/submodules/LegacyComponents/Sources/TGViewController.mm index db80d043bce..565c3585a9d 100644 --- a/submodules/LegacyComponents/Sources/TGViewController.mm +++ b/submodules/LegacyComponents/Sources/TGViewController.mm @@ -521,12 +521,10 @@ - (bool)inPopover - (bool)inFormSheet { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { return false; - else - { - if ([self.navigationController isKindOfClass:[TGNavigationController class]]) - { + } else { + if ([self.navigationController isKindOfClass:[TGNavigationController class]]) { switch (((TGNavigationController *)self.navigationController).presentationStyle) { case TGNavigationControllerPresentationStyleInFormSheet: diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 52cbe8cf301..ea428cb7bb2 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -222,7 +222,38 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu( + context: AccountContext, + peer: Peer?, + threadTitle: String?, + chatLocation: ChatLocation, + editMediaOptions: LegacyAttachmentMenuMediaEditing?, + addingMedia: Bool, + saveEditedPhotos: Bool, + allowGrouping: Bool, + hasSchedule: Bool, + canSendPolls: Bool, + updatedPresentationData: (initial: PresentationData, signal: Signal), + parentController: LegacyController, + recentlyUsedInlineBots: [Peer], + initialCaption: NSAttributedString, + openGallery: @escaping () -> Void, + openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, + openFileGallery: @escaping () -> Void, + openWebSearch: @escaping () -> Void, + openMap: @escaping () -> Void, + openContacts: @escaping () -> Void, + openPoll: @escaping () -> Void, + presentSelectionLimitExceeded: @escaping () -> Void, + presentCantSendMultipleFiles: @escaping () -> Void, + presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, + sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, + selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, + getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + present: @escaping (ViewController, Any?) -> Void +) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -382,7 +413,13 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + let galleryTitle: String + if addingMedia { + galleryTitle = presentationData.strings.AttachmentMenu_AddPhotoOrVideo + } else { + galleryTitle = editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo + } + let galleryItem = TGMenuSheetButtonItemView(title: galleryTitle, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! @@ -395,11 +432,19 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } } itemViews.append(galleryItem) - underlyingViews.append(galleryItem) + + if addingMedia { + let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_AddDocument, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + controller?.dismiss(animated: true) + openFileGallery() + })! + itemViews.append(fileItem) + underlyingViews.append(fileItem) + } } - if !editing { + if !editing && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -408,7 +453,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit underlyingViews.append(fileItem) } - if canEditFile { + if canEditFile && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -488,7 +533,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit itemViews.append(editCurrentItem) } - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openMap() @@ -520,7 +565,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit carouselItemView?.underlyingViews = underlyingViews - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { for i in 0 ..< min(20, recentlyUsedInlineBots.count) { let peer = recentlyUsedInlineBots[i] let addressName = peer.addressName diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 8b97f917a76..031dfe5508c 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -297,11 +297,11 @@ public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId: let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation) var fileAttributes: [TelegramMediaFileAttribute] = [] - fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { @@ -339,11 +339,11 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationI let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation) var fileAttributes: [TelegramMediaFileAttribute] = [] - fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { @@ -616,7 +616,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: format == .jxl ? "image/jxl" : "image/jpeg", size: nil, attributes: [ .FileName(fileName: format == .jxl ? "image\(sizeSide)-q\(quality).jxl" : "image\(sizeSide)-q\(quality).jpg"), .ImageSize(size: PixelDimensions(scaledSize)) - ]) + ], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { @@ -761,7 +761,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: fileSize(path), attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) @@ -814,7 +814,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max)) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) @@ -894,9 +894,9 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A finalDuration = duration } - if !asAnimation { + /*if !asAnimation { finalDimensions = TGFitSize(finalDimensions, CGSize(width: 848.0, height: 848.0)) - } + }*/ var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let thumbnail = thumbnail { @@ -981,7 +981,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A if !asFile { // MARK: Nicegram RoundedVideos, change to 'flags: videoFlags' - fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: videoFlags, preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil)) if let adjustments = adjustments { if adjustments.sendAsGif { fileAttributes.append(.Animated) @@ -1015,7 +1015,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A fileAttributes.append(.HasLinkedStickers) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index 59440be2d57..7b4b0095c4b 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -67,14 +67,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { } } if let action = self.action { - let actionColor: UIColor - switch self.actionType { - case .generic: - actionColor = self.theme.chatList.sectionHeaderTextColor - case .destructive: - actionColor = self.theme.list.itemDestructiveColor - } - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: actionColor) + self.updateActionTitle() self.actionButton?.accessibilityLabel = action self.actionButton?.accessibilityTraits = [.button] } @@ -115,16 +108,34 @@ public final class ListSectionHeaderNode: ASDisplayNode { return super.hitTest(point, with: event) } + private func updateActionTitle() { + guard let action = self.action else { + return + } + let actionColor: UIColor + switch self.actionType { + case .generic: + actionColor = self.theme.chatList.sectionHeaderTextColor + case .destructive: + actionColor = self.theme.list.itemDestructiveColor + } + let attributedText = NSMutableAttributedString(string: action, font: actionFont, textColor: actionColor) + if let range = attributedText.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + attributedText.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } + self.actionButtonLabel?.attributedText = attributedText + } + public func updateTheme(theme: PresentationTheme) { if self.theme !== theme { self.theme = theme + + self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor) - - self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor - if let action = self.action { - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) - } + + self.updateActionTitle() if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) diff --git a/submodules/MediaPasteboardUI/BUILD b/submodules/MediaPasteboardUI/BUILD index 85261b8c928..883909ef8ae 100644 --- a/submodules/MediaPasteboardUI/BUILD +++ b/submodules/MediaPasteboardUI/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/AttachmentUI:AttachmentUI", "//submodules/MediaPickerUI:MediaPickerUI", + "//submodules/AttachmentTextInputPanelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift index e4aea03f6f9..eaf4f3d86fa 100644 --- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift +++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift @@ -8,6 +8,7 @@ import AttachmentUI import MediaPickerUI import AccountContext import LegacyComponents +import AttachmentTextInputPanelNode public func mediaPasteboardScreen( context: AccountContext, @@ -15,9 +16,10 @@ public func mediaPasteboardScreen( peer: EnginePeer, subjects: [MediaPickerScreen.Subject.Media], presentMediaPicker: @escaping (_ subject: MediaPickerScreen.Subject, _ saveEditedPhotos: Bool, _ bannedSendPhotos: (Int32, Bool)?, _ bannedSendVideos: (Int32, Bool)?, _ present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void) -> Void, - getSourceRect: (() -> CGRect?)? = nil + getSourceRect: (() -> CGRect?)? = nil, + makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil } ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) controller.requestController = { _, present in presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in present(mediaPicker, mediaPickerContext) diff --git a/submodules/MediaPlayer/Package.swift b/submodules/MediaPlayer/Package.swift index a35bec3fb07..9c829fadc04 100644 --- a/submodules/MediaPlayer/Package.swift +++ b/submodules/MediaPlayer/Package.swift @@ -42,6 +42,8 @@ let package = Package( "MediaPlayerNode.swift", "MediaPlayerAudioRenderer.swift", "MediaPlayerFramePreview.swift", - "VideoPlayerProxy.swift"]), + "VideoPlayerProxy.swift", + "ChunkMediaPlayer.swift" + ]), ] ) diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift new file mode 100644 index 00000000000..007c6eb2472 --- /dev/null +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift @@ -0,0 +1,1202 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import CoreMedia +import TelegramCore +import TelegramAudio + +private struct ChunkMediaPlayerControlTimebase { + let timebase: CMTimebase + let isAudio: Bool +} + +private enum ChunkMediaPlayerPlaybackAction { + case play + case pause +} + +private final class ChunkMediaPlayerPartLoadedState { + let part: ChunkMediaPlayerPart + let frameSource: MediaFrameSource + var mediaBuffersDisposable: Disposable? + var mediaBuffers: MediaPlaybackBuffers? + var extraVideoFrames: ([MediaTrackFrame], CMTime)? + + init(part: ChunkMediaPlayerPart, frameSource: MediaFrameSource, mediaBuffers: MediaPlaybackBuffers?) { + self.part = part + self.frameSource = frameSource + self.mediaBuffers = mediaBuffers + } + + deinit { + self.mediaBuffersDisposable?.dispose() + } +} + +private final class ChunkMediaPlayerLoadedState { + var partStates: [ChunkMediaPlayerPartLoadedState] = [] + var controlTimebase: ChunkMediaPlayerControlTimebase? + var lostAudioSession: Bool = false +} + +private struct ChunkMediaPlayerSeekState { + let duration: Double +} + +private enum ChunkMediaPlayerState { + case paused + case playing +} + +public enum ChunkMediaPlayerActionAtEnd { + case loop((() -> Void)?) + case action(() -> Void) + case loopDisablingSound(() -> Void) + case stop +} + +public enum ChunkMediaPlayerPlayOnceWithSoundActionAtEnd { + case loop + case loopDisablingSound + case stop + case repeatIfNeeded +} + +public enum ChunkMediaPlayerStreaming { + case none + case conservative + case earlierStart + case story + + public var enabled: Bool { + if case .none = self { + return false + } else { + return true + } + } + + public var parameters: (Double, Double, Double) { + switch self { + case .none, .conservative: + return (1.0, 2.0, 3.0) + case .earlierStart: + return (1.0, 1.0, 2.0) + case .story: + return (0.25, 0.5, 1.0) + } + } + + public var isSeekable: Bool { + switch self { + case .none, .conservative, .earlierStart: + return true + case .story: + return false + } + } +} + +private final class MediaPlayerAudioRendererContext { + let renderer: MediaPlayerAudioRenderer + var requestedFrames = false + + init(renderer: MediaPlayerAudioRenderer) { + self.renderer = renderer + } +} + +public final class ChunkMediaPlayerPart { + public struct Id: Hashable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public let startTime: Double + public let endTime: Double + public let file: TempBoxFile + + public var id: Id { + return Id(rawValue: self.file.path) + } + + public init(startTime: Double, endTime: Double, file: TempBoxFile) { + self.startTime = startTime + self.endTime = endTime + self.file = file + } +} + +public final class ChunkMediaPlayerPartsState { + public let duration: Double? + public let parts: [ChunkMediaPlayerPart] + + public init(duration: Double?, parts: [ChunkMediaPlayerPart]) { + self.duration = duration + self.parts = parts + } +} + +private final class ChunkMediaPlayerContext { + private let queue: Queue + private let postbox: Postbox + private let audioSessionManager: ManagedAudioSession + + private var partsState = ChunkMediaPlayerPartsState(duration: nil, parts: []) + + private let video: Bool + private var enableSound: Bool + private var baseRate: Double + private var playAndRecord: Bool + private var soundMuted: Bool + private var ambient: Bool + private var mixWithOthers: Bool + private var keepAudioSessionWhilePaused: Bool + private var continuePlayingWithoutSoundOnLostAudioSession: Bool + private let isAudioVideoMessage: Bool + private let onSeeked: () -> Void + + private var seekId: Int = 0 + private var initialSeekTimestamp: Double? + private var notifySeeked: Bool = false + + private let loadedState: ChunkMediaPlayerLoadedState + private var isSeeking: Bool = false + private var state: ChunkMediaPlayerState = .paused + private var audioRenderer: MediaPlayerAudioRendererContext? + private var forceAudioToSpeaker = false + fileprivate let videoRenderer: VideoPlayerProxy + + private var tickTimer: SwiftSignalKit.Timer? + + private var lastStatusUpdateTimestamp: Double? + private let playerStatus: Promise + private let playerStatusValue = Atomic(value: nil) + private let audioLevelPipe: ValuePipe + + fileprivate var actionAtEnd: ChunkMediaPlayerActionAtEnd = .stop + + private var stoppedAtEnd = false + + private var partsDisposable: Disposable? + + init( + queue: Queue, + postbox: Postbox, + audioSessionManager: ManagedAudioSession, + playerStatus: Promise, + audioLevelPipe: ValuePipe, + partsState: Signal, + video: Bool, + playAutomatically: Bool, + enableSound: Bool, + baseRate: Double, + playAndRecord: Bool, + soundMuted: Bool, + ambient: Bool, + mixWithOthers: Bool, + keepAudioSessionWhilePaused: Bool, + continuePlayingWithoutSoundOnLostAudioSession: Bool, + isAudioVideoMessage: Bool, + onSeeked: @escaping () -> Void + ) { + assert(queue.isCurrent()) + + self.queue = queue + self.postbox = postbox + self.audioSessionManager = audioSessionManager + self.playerStatus = playerStatus + self.audioLevelPipe = audioLevelPipe + self.video = video + self.enableSound = enableSound + self.baseRate = baseRate + self.playAndRecord = playAndRecord + self.soundMuted = soundMuted + self.ambient = ambient + self.mixWithOthers = mixWithOthers + self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused + self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession + self.isAudioVideoMessage = isAudioVideoMessage + self.onSeeked = onSeeked + + self.videoRenderer = VideoPlayerProxy(queue: queue) + + self.loadedState = ChunkMediaPlayerLoadedState() + + let queue = self.queue + let audioRendererContext = MediaPlayerAudioRenderer( + audioSession: .manager(self.audioSessionManager), + forAudioVideoMessage: self.isAudioVideoMessage, + playAndRecord: self.playAndRecord, + soundMuted: self.soundMuted, + ambient: self.ambient, + mixWithOthers: self.mixWithOthers, + forceAudioToSpeaker: self.forceAudioToSpeaker, + baseRate: self.baseRate, + audioLevelPipe: self.audioLevelPipe, + updatedRate: { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + }, + audioPaused: { [weak self] in + queue.async { + guard let self else { + return + } + if self.enableSound { + if self.continuePlayingWithoutSoundOnLostAudioSession { + self.continuePlayingWithoutSound(seek: .start) + } else { + self.pause(lostAudioSession: true, faded: false) + } + } else { + self.seek(timestamp: 0.0, action: .play, notify: true) + } + } + } + ) + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: audioRendererContext) + + self.loadedState.controlTimebase = ChunkMediaPlayerControlTimebase(timebase: audioRendererContext.audioTimebase, isAudio: true) + + self.videoRenderer.visibilityUpdated = { [weak self] value in + assert(queue.isCurrent()) + + if let strongSelf = self, !strongSelf.enableSound || strongSelf.continuePlayingWithoutSoundOnLostAudioSession { + switch strongSelf.state { + case .paused: + if value { + strongSelf.play() + } + case .playing: + if !value { + strongSelf.pause(lostAudioSession: false) + } + } + } + } + + self.videoRenderer.takeFrameAndQueue = (queue, { [weak self] in + assert(queue.isCurrent()) + + guard let self else { + return .noFrames + } + + var ignoreEmptyExtraFrames = false + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + + if let (extraVideoFrames, atTime) = partState.extraVideoFrames { + partState.extraVideoFrames = nil + + if extraVideoFrames.isEmpty { + if !ignoreEmptyExtraFrames { + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: i != 0) + } + } else { + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: i != 0) + } + } + + if let videoBuffer = partState.mediaBuffers?.videoBuffer { + let frame = videoBuffer.takeFrame() + switch frame { + case .finished: + ignoreEmptyExtraFrames = true + continue + default: + if ignoreEmptyExtraFrames, case let .frame(mediaTrackFrame) = frame { + return .restoreState(frames: [mediaTrackFrame], atTimestamp: mediaTrackFrame.position, soft: i != 0) + } + + return frame + } + } + } + + return .noFrames + }) + + audioRendererContext.start() + self.tick() + + let tickTimer = SwiftSignalKit.Timer(timeout: 1.0 / 25.0, repeat: true, completion: { [weak self] in + self?.tick() + }, queue: self.queue) + self.tickTimer = tickTimer + tickTimer.start() + + self.partsDisposable = (partsState |> deliverOn(self.queue)).startStrict(next: { [weak self] partsState in + guard let self else { + return + } + self.partsState = partsState + self.tick() + }) + } + + deinit { + assert(self.queue.isCurrent()) + + self.tickTimer?.invalidate() + self.partsDisposable?.dispose() + } + + fileprivate func seek(timestamp: Double, notify: Bool) { + assert(self.queue.isCurrent()) + + let action: ChunkMediaPlayerPlaybackAction + switch self.state { + case .paused: + action = .pause + case .playing: + action = .play + } + self.seek(timestamp: timestamp, action: action, notify: notify) + } + + fileprivate func seek(timestamp: Double, action: ChunkMediaPlayerPlaybackAction, notify: Bool) { + assert(self.queue.isCurrent()) + + self.isSeeking = true + self.loadedState.partStates.removeAll() + + self.seekId += 1 + self.initialSeekTimestamp = timestamp + self.notifySeeked = true + + switch action { + case .play: + self.state = .playing + case .pause: + self.state = .paused + } + + self.videoRenderer.flush() + + if let audioRenderer = self.audioRenderer { + let queue = self.queue + audioRenderer.renderer.flushBuffers(at: CMTime(seconds: timestamp, preferredTimescale: 44100), completion: { [weak self] in + queue.async { + guard let self else { + return + } + self.isSeeking = false + self.tick() + } + }) + } else { + if let controlTimebase = self.loadedState.controlTimebase, !controlTimebase.isAudio { + CMTimebaseSetTime(controlTimebase.timebase, time: CMTimeMakeWithSeconds(timestamp, preferredTimescale: 44000)) + } + + self.isSeeking = false + self.tick() + } + } + + fileprivate func play() { + assert(self.queue.isCurrent()) + + if case .paused = self.state { + self.state = .playing + self.stoppedAtEnd = false + self.lastStatusUpdateTimestamp = nil + + if self.enableSound { + self.audioRenderer?.renderer.start() + } + + let timestamp: Double + if let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + } else { + timestamp = self.initialSeekTimestamp ?? 0.0 + } + + self.seek(timestamp: timestamp, action: .play, notify: false) + } + } + + fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) { + assert(self.queue.isCurrent()) + + /*#if DEBUG + var seek = seek + if case .timecode = seek { + seek = .timecode(830.83000000000004) + } + #endif*/ + + if !self.enableSound { + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + self.playAndRecord = playAndRecord + + var timestamp: Double + if case let .timecode(time) = seek { + timestamp = time + } else if case .none = seek, let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + if let duration = self.currentDuration(), duration != 0.0 { + if timestamp > duration - 2.0 { + timestamp = 0.0 + } + } + } else { + timestamp = 0.0 + } + self.seek(timestamp: timestamp, action: .play, notify: true) + } else { + if case let .timecode(time) = seek { + self.seek(timestamp: Double(time), action: .play, notify: true) + } else if case .playing = self.state { + } else { + self.play() + } + } + + self.stoppedAtEnd = false + } + + fileprivate func setSoundMuted(soundMuted: Bool) { + self.soundMuted = soundMuted + self.audioRenderer?.renderer.setSoundMuted(soundMuted: soundMuted) + } + + fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) { + if self.ambient != isAmbient { + self.ambient = isAmbient + self.audioRenderer?.renderer.reconfigureAudio(ambient: self.ambient) + } + } + + fileprivate func continuePlayingWithoutSound(seek: MediaPlayerSeek) { + if self.enableSound { + self.lastStatusUpdateTimestamp = nil + + if let controlTimebase = self.loadedState.controlTimebase { + self.enableSound = false + self.playAndRecord = false + + var timestamp: Double + if case let .timecode(time) = seek { + timestamp = time + } else if case .none = seek { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + if let duration = self.currentDuration(), duration != 0.0 { + if timestamp > duration - 2.0 { + timestamp = 0.0 + } + } + } else { + timestamp = 0.0 + } + + self.seek(timestamp: timestamp, action: .play, notify: true) + } + } + } + + fileprivate func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + if self.continuePlayingWithoutSoundOnLostAudioSession != value { + self.continuePlayingWithoutSoundOnLostAudioSession = value + } + } + + fileprivate func setBaseRate(_ baseRate: Double) { + self.baseRate = baseRate + self.lastStatusUpdateTimestamp = nil + self.tick() + self.audioRenderer?.renderer.setBaseRate(baseRate) + } + + fileprivate func setForceAudioToSpeaker(_ value: Bool) { + if self.forceAudioToSpeaker != value { + self.forceAudioToSpeaker = value + + self.audioRenderer?.renderer.setForceAudioToSpeaker(value) + } + } + + fileprivate func setKeepAudioSessionWhilePaused(_ value: Bool) { + if self.keepAudioSessionWhilePaused != value { + self.keepAudioSessionWhilePaused = value + + var isPlaying = false + switch self.state { + case .playing: + isPlaying = true + default: + break + } + if value && !isPlaying { + self.audioRenderer?.renderer.stop() + } else { + self.audioRenderer?.renderer.start() + } + } + } + + fileprivate func pause(lostAudioSession: Bool, faded: Bool = false) { + assert(self.queue.isCurrent()) + + if lostAudioSession { + self.loadedState.lostAudioSession = true + } + switch self.state { + case .paused: + break + case .playing: + self.state = .paused + self.lastStatusUpdateTimestamp = nil + + self.tick() + } + } + + fileprivate func togglePlayPause(faded: Bool) { + assert(self.queue.isCurrent()) + + switch self.state { + case .paused: + if !self.enableSound { + self.playOnceWithSound(playAndRecord: false, seek: .none) + } else { + self.play() + } + case .playing: + self.pause(lostAudioSession: false, faded: faded) + } + } + + private func currentDuration() -> Double? { + return self.partsState.duration + } + + private func tick() { + if self.isSeeking { + return + } + + var timestamp: Double + if let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + } else { + timestamp = self.initialSeekTimestamp ?? 0.0 + } + timestamp = max(0.0, timestamp) + + if let firstPart = self.loadedState.partStates.first, let mediaBuffers = firstPart.mediaBuffers, mediaBuffers.videoBuffer != nil, mediaBuffers.audioBuffer == nil { + // No audio + if self.audioRenderer != nil { + self.audioRenderer?.renderer.stop() + self.audioRenderer = nil + + var timebase: CMTimebase? + CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase) + let controlTimebase = ChunkMediaPlayerControlTimebase(timebase: timebase!, isAudio: false) + CMTimebaseSetTime(timebase!, time: CMTimeMakeWithSeconds(timestamp, preferredTimescale: 44000)) + + self.loadedState.controlTimebase = controlTimebase + } + } + + //print("Timestamp: \(timestamp)") + + var duration: Double = 0.0 + if let partsStateDuration = self.partsState.duration { + duration = partsStateDuration + } + + var validParts: [ChunkMediaPlayerPart] = [] + + for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + var partMatches = false + if timestamp >= part.startTime - 0.5 && timestamp < part.endTime + 0.5 { + partMatches = true + } + + if partMatches { + validParts.append(part) + } + } + if let lastValidPart = validParts.last { + for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + if lastValidPart !== part && part.startTime > lastValidPart.startTime && part.startTime <= lastValidPart.endTime + 0.5 { + validParts.append(part) + break + } + } + } + + /*for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + var partMatches = false + if timestamp >= part.startTime - 0.001 && timestamp < part.endTime - 0.001 { + partMatches = true + } else if part.startTime < 0.2 && timestamp < part.endTime - 0.001 { + partMatches = true + } + + if !partMatches, i != self.partsState.parts.count - 1, part.startTime >= 0.001, timestamp >= part.startTime { + let nextPart = self.partsState.parts[i + 1] + if timestamp < nextPart.endTime - 0.001 { + if part.endTime >= nextPart.startTime - 0.1 { + partMatches = true + } + } + } + + if partMatches { + validParts.append(part) + + inner: for lookaheadPart in self.partsState.parts { + if lookaheadPart.startTime >= part.endTime - 0.001 && lookaheadPart.startTime - 0.1 < part.endTime { + validParts.append(lookaheadPart) + break inner + } + } + + break + } + }*/ + + if validParts.isEmpty, let initialSeekTimestamp = self.initialSeekTimestamp { + for part in self.partsState.parts { + if initialSeekTimestamp >= part.startTime - 0.2 && initialSeekTimestamp < part.endTime { + self.initialSeekTimestamp = nil + + self.videoRenderer.flush() + + if let audioRenderer = self.audioRenderer { + self.isSeeking = true + let queue = self.queue + audioRenderer.renderer.flushBuffers(at: CMTime(seconds: part.startTime + 0.1, preferredTimescale: 44100), completion: { [weak self] in + queue.async { + guard let self else { + return + } + self.isSeeking = false + self.tick() + } + }) + } + + return + } + } + } else { + self.initialSeekTimestamp = nil + } + + self.loadedState.partStates.removeAll(where: { partState in + if !validParts.contains(where: { $0.id == partState.part.id }) { + return true + } + return false + }) + + for part in validParts { + if !self.loadedState.partStates.contains(where: { $0.part.id == part.id }) { + let frameSource = FFMpegMediaFrameSource( + queue: self.queue, + postbox: self.postbox, + userLocation: .other, + userContentType: .other, + resourceReference: .standalone(resource: LocalFileReferenceMediaResource(localFilePath: "", randomId: 0)), + tempFilePath: part.file.path, + limitedFileRange: nil, + streamable: false, + isSeekable: true, + video: self.video, + preferSoftwareDecoding: false, + fetchAutomatically: false, + stallDuration: 1.0, + lowWaterDuration: 2.0, + highWaterDuration: 3.0, + storeAfterDownload: nil + ) + + let partState = ChunkMediaPlayerPartLoadedState( + part: part, + frameSource: frameSource, + mediaBuffers: nil + ) + self.loadedState.partStates.append(partState) + self.loadedState.partStates.sort(by: { $0.part.startTime < $1.part.startTime }) + } + } + + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + if partState.mediaBuffersDisposable == nil { + partState.mediaBuffersDisposable = (partState.frameSource.seek(timestamp: i == 0 ? timestamp : 0.0) + |> deliverOn(self.queue)).startStrict(next: { [weak self, weak partState] result in + guard let self, let partState else { + return + } + guard let result = result.unsafeGet() else { + return + } + + partState.mediaBuffers = result.buffers + partState.extraVideoFrames = (result.extraDecodedVideoFrames, result.timestamp) + + if partState === self.loadedState.partStates.first { + self.audioRenderer?.renderer.flushBuffers(at: result.timestamp, completion: {}) + } + + let queue = self.queue + result.buffers.audioBuffer?.statusUpdated = { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + } + result.buffers.videoBuffer?.statusUpdated = { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + } + + self.tick() + }) + } + } + + var videoStatus: MediaTrackFrameBufferStatus? + var audioStatus: MediaTrackFrameBufferStatus? + + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + + var partVideoStatus: MediaTrackFrameBufferStatus? + var partAudioStatus: MediaTrackFrameBufferStatus? + if let videoTrackFrameBuffer = partState.mediaBuffers?.videoBuffer { + partVideoStatus = videoTrackFrameBuffer.status(at: i == 0 ? timestamp : videoTrackFrameBuffer.startTime.seconds) + } + if let audioTrackFrameBuffer = partState.mediaBuffers?.audioBuffer { + partAudioStatus = audioTrackFrameBuffer.status(at: i == 0 ? timestamp : audioTrackFrameBuffer.startTime.seconds) + } + if i == 0 { + videoStatus = partVideoStatus + audioStatus = partAudioStatus + } + } + + var performActionAtEndNow = false + + var worstStatus: MediaTrackFrameBufferStatus? + for status in [videoStatus, audioStatus] { + if let status = status { + if let worst = worstStatus { + switch status { + case .buffering: + worstStatus = status + case let .full(currentFullUntil): + switch worst { + case .buffering: + worstStatus = worst + case let .full(worstFullUntil): + if currentFullUntil < worstFullUntil { + worstStatus = status + } else { + worstStatus = worst + } + case .finished: + worstStatus = status + } + case let .finished(currentFinishedAt): + switch worst { + case .buffering, .full: + worstStatus = worst + case let .finished(worstFinishedAt): + if currentFinishedAt < worstFinishedAt { + worstStatus = worst + } else { + worstStatus = status + } + } + } + } else { + worstStatus = status + } + } + } + + var rate: Double + var bufferingProgress: Float? + + if let worstStatus = worstStatus, case let .full(fullUntil) = worstStatus, fullUntil.isFinite { + var playing = false + if case .playing = self.state { + playing = true + } + if playing { + rate = self.baseRate + } else { + rate = 0.0 + } + } else if let worstStatus = worstStatus, case let .finished(finishedAt) = worstStatus, finishedAt.isFinite { + var playing = false + if case .playing = self.state { + playing = true + } + if playing { + rate = self.baseRate + } else { + rate = 0.0 + } + + //print("finished timestamp: \(timestamp), finishedAt: \(finishedAt), duration: \(duration)") + if duration > 0.0 && timestamp >= finishedAt && finishedAt >= duration - 0.2 { + performActionAtEndNow = true + } + } else if case .buffering = worstStatus { + bufferingProgress = 0.0 + rate = 0.0 + } else { + rate = 0.0 + bufferingProgress = 0.0 + } + + if rate != 0.0 && self.initialSeekTimestamp != nil { + self.initialSeekTimestamp = nil + } + + if duration > 0.0 && timestamp >= duration { + performActionAtEndNow = true + } + + var reportRate = rate + + if let controlTimebase = self.loadedState.controlTimebase { + if controlTimebase.isAudio { + if !rate.isZero { + self.audioRenderer?.renderer.start() + } + self.audioRenderer?.renderer.setRate(rate) + if !rate.isZero, let audioRenderer = self.audioRenderer { + let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase) + if !timebaseRate.isEqual(to: rate) { + reportRate = timebaseRate + } + } + } else { + if !CMTimebaseGetRate(controlTimebase.timebase).isEqual(to: rate) { + CMTimebaseSetRate(controlTimebase.timebase, rate: rate) + } + } + } + + if let controlTimebase = self.loadedState.controlTimebase, let videoTrackFrameBuffer = self.loadedState.partStates.first?.mediaBuffers?.videoBuffer, videoTrackFrameBuffer.hasFrames { + self.videoRenderer.state = (controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect) + } + + if let audioRenderer = self.audioRenderer { + let queue = self.queue + audioRenderer.requestedFrames = true + audioRenderer.renderer.beginRequestingFrames(queue: queue.queue, takeFrame: { [weak self] in + assert(queue.isCurrent()) + guard let self else { + return .noFrames + } + + for partState in self.loadedState.partStates { + if let audioTrackFrameBuffer = partState.mediaBuffers?.audioBuffer { + let frame = audioTrackFrameBuffer.takeFrame() + switch frame { + case .finished: + continue + default: + /*if case let .frame(frame) = frame { + print("audio: \(frame.position.seconds) \(frame.position.value) next: (\(frame.position.value + frame.duration.value))") + }*/ + return frame + } + } + } + + return .noFrames + }) + } + + var statusTimestamp = CACurrentMediaTime() + let playbackStatus: MediaPlayerPlaybackStatus + var isPlaying = false + var isPaused = false + if case .playing = self.state { + isPlaying = true + } else if case .paused = self.state { + isPaused = true + } + if let bufferingProgress = bufferingProgress { + playbackStatus = .buffering(initial: false, whilePlaying: isPlaying, progress: Float(bufferingProgress), display: true) + } else if !rate.isZero { + if reportRate.isZero { + playbackStatus = .playing + statusTimestamp = 0.0 + } else { + playbackStatus = .playing + } + } else { + if performActionAtEndNow && !self.stoppedAtEnd, case .loop = self.actionAtEnd, isPlaying { + playbackStatus = .playing + } else { + playbackStatus = .paused + } + } + let _ = isPaused + + if self.lastStatusUpdateTimestamp == nil || self.lastStatusUpdateTimestamp! < statusTimestamp + 1.0 / 25.0 { + self.lastStatusUpdateTimestamp = statusTimestamp + let reportTimestamp = timestamp + let statusTimestamp: Double + if duration == 0.0 { + statusTimestamp = max(reportTimestamp, 0.0) + } else { + statusTimestamp = min(max(reportTimestamp, 0.0), duration) + } + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: statusTimestamp, baseRate: self.baseRate, seekId: self.seekId, status: playbackStatus, soundEnabled: self.enableSound) + self.playerStatus.set(.single(status)) + let _ = self.playerStatusValue.swap(status) + } + + if self.notifySeeked { + self.notifySeeked = false + self.onSeeked() + } + + if performActionAtEndNow { + if !self.stoppedAtEnd { + switch self.actionAtEnd { + case let .loop(f): + self.stoppedAtEnd = false + self.seek(timestamp: 0.0, action: .play, notify: true) + f?() + case .stop: + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + case let .action(f): + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + f() + case let .loopDisablingSound(f): + self.stoppedAtEnd = false + self.enableSound = false + self.seek(timestamp: 0.0, action: .play, notify: true) + f() + } + } + } + } +} + +public final class ChunkMediaPlayer { + private let queue = Queue() + private var contextRef: Unmanaged? + + private let statusValue = Promise() + + public var status: Signal { + return self.statusValue.get() + } + + private let audioLevelPipe = ValuePipe() + public var audioLevelEvents: Signal { + return self.audioLevelPipe.signal() + } + + public var actionAtEnd: ChunkMediaPlayerActionAtEnd = .stop { + didSet { + let value = self.actionAtEnd + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.actionAtEnd = value + } + } + } + } + + public init( + postbox: Postbox, + audioSessionManager: ManagedAudioSession, + partsState: Signal, + video: Bool, + playAutomatically: Bool = false, + enableSound: Bool, + baseRate: Double = 1.0, + playAndRecord: Bool = false, + soundMuted: Bool = false, + ambient: Bool = false, + mixWithOthers: Bool = false, + keepAudioSessionWhilePaused: Bool = false, + continuePlayingWithoutSoundOnLostAudioSession: Bool = false, + isAudioVideoMessage: Bool = false, + onSeeked: (() -> Void)? = nil + ) { + let audioLevelPipe = self.audioLevelPipe + self.queue.async { + let context = ChunkMediaPlayerContext( + queue: self.queue, + postbox: postbox, + audioSessionManager: audioSessionManager, + playerStatus: self.statusValue, + audioLevelPipe: audioLevelPipe, + partsState: partsState, + video: video, + playAutomatically: playAutomatically, + enableSound: enableSound, + baseRate: baseRate, + playAndRecord: playAndRecord, + soundMuted: soundMuted, + ambient: ambient, + mixWithOthers: mixWithOthers, + keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, + continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, + isAudioVideoMessage: isAudioVideoMessage, + onSeeked: { + onSeeked?() + } + ) + self.contextRef = Unmanaged.passRetained(context) + } + } + + deinit { + let contextRef = self.contextRef + self.queue.async { + contextRef?.release() + } + } + + public func play() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.play() + } + } + } + + public func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) + } + } + } + + public func setSoundMuted(soundMuted: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setSoundMuted(soundMuted: soundMuted) + } + } + } + + public func continueWithOverridingAmbientMode(isAmbient: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continueWithOverridingAmbientMode(isAmbient: isAmbient) + } + } + } + + public func continuePlayingWithoutSound(seek: MediaPlayerSeek = .start) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continuePlayingWithoutSound(seek: seek) + } + } + } + + public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setContinuePlayingWithoutSoundOnLostAudioSession(value) + } + } + } + + public func setForceAudioToSpeaker(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setForceAudioToSpeaker(value) + } + } + } + + public func setKeepAudioSessionWhilePaused(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setKeepAudioSessionWhilePaused(value) + } + } + } + + public func pause() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.pause(lostAudioSession: false) + } + } + } + + public func togglePlayPause(faded: Bool = false) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.togglePlayPause(faded: faded) + } + } + } + + public func seek(timestamp: Double, play: Bool? = nil) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + if let play { + context.seek(timestamp: timestamp, action: play ? .play : .pause, notify: false) + } else { + context.seek(timestamp: timestamp, notify: false) + } + } + } + } + + public func setBaseRate(_ baseRate: Double) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setBaseRate(baseRate) + } + } + } + + public func attachPlayerNode(_ node: MediaPlayerNode) { + let nodeRef: Unmanaged = Unmanaged.passRetained(node) + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.videoRenderer.attachNodeAndRelease(nodeRef) + } else { + Queue.mainQueue().async { + nodeRef.release() + } + } + } + } +} diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift index 8a6e6c7b706..d161b921e91 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift @@ -72,6 +72,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? + private let limitedFileRange: Range? private let streamable: Bool private let isSeekable: Bool private let stallDuration: Double @@ -102,13 +103,14 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0, storeAfterDownload: (() -> Void)? = nil) { + public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0, storeAfterDownload: (() -> Void)? = nil) { self.queue = queue self.postbox = postbox self.userLocation = userLocation self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.isSeekable = isSeekable self.video = video @@ -187,6 +189,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let postbox = self.postbox let resourceReference = self.resourceReference let tempFilePath = self.tempFilePath + let limitedFileRange = self.limitedFileRange let queue = self.queue let streamable = self.streamable let isSeekable = self.isSeekable @@ -198,7 +201,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let storeAfterDownload = self.storeAfterDownload self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) let (frames, endOfStream) = context.takeFrames(until: timestamp, types: types) @@ -242,6 +245,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let userLocation = self.userLocation let resourceReference = self.resourceReference let tempFilePath = self.tempFilePath + let limitedFileRange = self.limitedFileRange let streamable = self.streamable let isSeekable = self.isSeekable let video = self.video @@ -259,7 +263,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.performWithContext { [weak self] context in let _ = currentSemaphore.swap(context.currentSemaphore) - context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) context.seek(timestamp: timestamp, completed: { streamDescriptionsAndTimestamp in queue.async { @@ -272,12 +276,12 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { var videoBuffer: MediaTrackFrameBuffer? if let audio = streamDescriptions.audio { - audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) + audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, startTime: audio.startTime, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) } var extraDecodedVideoFrames: [MediaTrackFrame] = [] if let video = streamDescriptions.video { - videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) + videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, startTime: video.startTime, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) for videoFrame in streamDescriptions.extraVideoFrames { if let decodedFrame = video.decoder.decode(frame: videoFrame) { extraDecodedVideoFrames.append(decodedFrame) diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index 2cbbed178af..83b92c97cf0 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -10,6 +10,7 @@ private struct StreamContext { let codecContext: FFMpegAVCodecContext? let fps: CMTime let timebase: CMTime + let startTime: CMTime let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double @@ -17,6 +18,7 @@ private struct StreamContext { } struct FFMpegMediaFrameSourceDescription { + let startTime: CMTime let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double @@ -63,15 +65,12 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa } var fetchedCount: Int32 = 0 - var fetchedData: Data? - /*#if DEBUG - maxOffset = max(maxOffset, context.readingOffset + Int(bufferSize)) - print("maxOffset \(maxOffset)") - #endif*/ - - let resourceSize: Int64 = resourceReference.resource.size ?? (Int64.max - 1) + var resourceSize: Int64 = resourceReference.resource.size ?? (Int64.max - 1) + if let limitedFileRange = context.limitedFileRange { + resourceSize = min(resourceSize, limitedFileRange.upperBound) + } let readCount = max(0, min(resourceSize - context.readingOffset, Int64(bufferSize))) let requestRange: Range = context.readingOffset ..< (context.readingOffset + readCount) @@ -95,9 +94,6 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa if readCount == 0 { fetchedData = Data() } else { - #if DEBUG - //print("requestRange: \(requestRange)") - #endif if let tempFilePath = context.tempFilePath, let fileData = (try? Data(contentsOf: URL(fileURLWithPath: tempFilePath), options: .mappedRead))?.subdata(in: Int(requestRange.lowerBound) ..< Int(requestRange.upperBound)) { fetchedData = fileData } else { @@ -205,7 +201,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe var result: Int64 = offset - let resourceSize: Int64 + var resourceSize: Int64 if let size = resourceReference.resource.size { resourceSize = size } else { @@ -238,6 +234,9 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe resourceSize = Int64.max - 1 } } + if let limitedFileRange = context.limitedFileRange { + resourceSize = min(resourceSize, limitedFileRange.upperBound) + } if (whence & FFMPEG_AVSEEK_SIZE) != 0 { result = Int64(resourceSize == Int(Int32.max - 1) ? 0 : resourceSize) @@ -252,10 +251,21 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe } else { if streamable { if context.tempFilePath == nil { - let fetchRange: Range = context.readingOffset ..< Int64.max - context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + let fetchRange: Range? + if let limitedFileRange = context.limitedFileRange { + if context.readingOffset < limitedFileRange.upperBound { + fetchRange = context.readingOffset ..< limitedFileRange.upperBound + } else { + fetchRange = nil + } + } else { + fetchRange = context.readingOffset ..< Int64.max + } + if let fetchRange { + context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + } } - } else if !context.requestedCompleteFetch && context.fetchAutomatically { + } else if !context.requestedCompleteFetch && context.fetchAutomatically && context.limitedFileRange == nil { context.requestedCompleteFetch = true if context.tempFilePath == nil { context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) @@ -283,6 +293,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fileprivate var userContentType: MediaResourceUserContentType? fileprivate var resourceReference: MediaResourceReference? fileprivate var tempFilePath: String? + fileprivate var limitedFileRange: Range? fileprivate var streamable: Bool? fileprivate var statsCategory: MediaResourceStatsCategory? @@ -327,16 +338,22 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.autosaveDisposable.dispose() } - func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?, storeAfterDownload: (() -> Void)?) { + func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?, storeAfterDownload: (() -> Void)?) { if self.readingError || self.initializedState != nil { return } let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + var streamable = streamable + if limitedFileRange != nil { + streamable = true + } + self.postbox = postbox self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.statsCategory = video ? .video : .audio self.userLocation = userLocation @@ -381,7 +398,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { } if streamable { - if self.tempFilePath == nil { + if self.tempFilePath == nil && limitedFileRange == nil { self.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } } else if !self.requestedCompleteFetch && self.fetchAutomatically { @@ -429,6 +446,14 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = CMTimeMake(value: Int64.min, timescale: duration.timescale) } + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + let metrics = avFormatContext.metricsForStream(at: streamIndex) let rotationAngle: Double = metrics.rotationAngle @@ -439,24 +464,24 @@ final class FFMpegMediaFrameSourceContext: NSObject { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { - videoStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } } } } else if codecId == FFMpegCodecIdMPEG4 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } else if codecId == FFMpegCodecIdH264 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } else if codecId == FFMpegCodecIdHEVC { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromHEVCCodecData(UInt32(kCMVideoCodecType_HEVC), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } @@ -484,7 +509,15 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = CMTimeMake(value: Int64.min, timescale: duration.timescale) } - audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0) + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + + audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0) break } } @@ -493,7 +526,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.initializedState = InitializedState(avIoContext: avIoContext, avFormatContext: avFormatContext, audioStream: audioStream, videoStream: videoStream) - if streamable { + if streamable && limitedFileRange == nil { if self.tempFilePath == nil { self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } @@ -620,11 +653,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { var videoDescription: FFMpegMediaFrameSourceDescription? if let audioStream = initializedState.audioStream { - audioDescription = FFMpegMediaFrameSourceDescription(duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0) + audioDescription = FFMpegMediaFrameSourceDescription(startTime: audioStream.startTime, duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0) } if let videoStream = initializedState.videoStream { - videoDescription = FFMpegMediaFrameSourceDescription(duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect) + videoDescription = FFMpegMediaFrameSourceDescription(startTime: videoStream.startTime, duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect) } var actualPts: CMTime = CMTimeMake(value: 0, timescale: 1) diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 20ec8f5f5d6..c2e23f67431 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -117,6 +117,7 @@ private final class MediaPlayerContext { private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? + private let limitedFileRange: Range? private let streamable: MediaPlayerStreaming private let video: Bool private let preferSoftwareDecoding: Bool @@ -151,7 +152,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -163,6 +164,7 @@ private final class MediaPlayerContext { self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.video = video self.preferSoftwareDecoding = preferSoftwareDecoding @@ -232,7 +234,7 @@ private final class MediaPlayerContext { if let loadedState = maybeLoadedState, let videoBuffer = loadedState.mediaBuffers.videoBuffer { if let (extraVideoFrames, atTime) = loadedState.extraVideoFrames { loadedState.extraVideoFrames = nil - return .restoreState(extraVideoFrames, atTime) + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: false) } else { return videoBuffer.takeFrame() } @@ -340,7 +342,7 @@ private final class MediaPlayerContext { let _ = self.playerStatusValue.swap(status) } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, userLocation: self.userLocation, userContentType: self.userContentType, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, streamable: self.streamable.enabled, isSeekable: self.streamable.isSeekable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2, storeAfterDownload: self.storeAfterDownload) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, userLocation: self.userLocation, userContentType: self.userContentType, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, limitedFileRange: self.limitedFileRange, streamable: self.streamable.enabled, isSeekable: self.streamable.isSeekable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2, storeAfterDownload: self.storeAfterDownload) let disposable = MetaDisposable() let updatedSeekState: MediaPlayerSeekState? if let loadedDuration = loadedDuration { @@ -1128,10 +1130,10 @@ public final class MediaPlayer { } } - public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, soundMuted: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { + public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, limitedFileRange: Range? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, soundMuted: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { let audioLevelPipe = self.audioLevelPipe self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift index 08be7b2aca7..b98e506f65b 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift @@ -96,7 +96,12 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U if !didSetRate { context.state = .playing(rate: rate, didSetRate: true) let masterClock = CMTimebaseCopySource(context.timebase) - CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: CMTimeMake(value: sampleIndex, timescale: 44100), immediateSourceTime: CMSyncGetTime(masterClock)) + let anchorTime = CMTimeMake(value: sampleIndex, timescale: 44100) + let immediateSourceTime = CMSyncGetTime(masterClock) + if anchorTime.seconds < CMTimebaseGetTime(context.timebase).seconds - 0.5 { + assert(true) + } + CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: anchorTime, immediateSourceTime: immediateSourceTime) updatedRate = context.updatedRate } else { context.renderTimestampTick += 1 @@ -165,6 +170,10 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U break } } + } else { + #if DEBUG + print("No audio data") + #endif } if !context.notifiedLowWater { diff --git a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift index 76e2c06b367..c9ba67c6337 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift @@ -179,13 +179,16 @@ public final class MediaPlayerNode: ASDisplayNode { var state = state takeFrameQueue.async { [weak node] in - switch takeFrame() { - case let .restoreState(frames, atTime): - Queue.mainQueue().async { - guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { - return + let takeFrameResult = takeFrame() + switch takeFrameResult { + case let .restoreState(frames, atTime, soft): + if !soft { + Queue.mainQueue().async { + guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() } - videoLayer.flush() } for i in 0 ..< frames.count { let frame = frames[i] @@ -193,15 +196,17 @@ public final class MediaPlayerNode: ASDisplayNode { state.maxTakenTime = frameTime let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray let dict = attachments[0] as! NSMutableDictionary - if i == 0 { + if i == 0 && !soft { CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) } - if CMTimeCompare(frame.position, atTime) < 0 { - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) - } else if CMTimeCompare(frame.position, atTime) == 0 { - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + if !soft { + if CMTimeCompare(frame.position, atTime) < 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + } else if CMTimeCompare(frame.position, atTime) == 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + } } Queue.mainQueue().async { guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { diff --git a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift index 01f76b8b5fb..d23fa4c77f4 100644 --- a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift +++ b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift @@ -11,7 +11,7 @@ public enum MediaTrackFrameBufferStatus { public enum MediaTrackFrameResult { case noFrames case skipFrame - case restoreState([MediaTrackFrame], CMTime) + case restoreState(frames: [MediaTrackFrame], atTimestamp: CMTime, soft: Bool) case frame(MediaTrackFrame) case finished } @@ -32,6 +32,7 @@ public final class MediaTrackFrameBuffer { private let frameSource: MediaFrameSource private let decoder: MediaTrackFrameDecoder private let type: MediaTrackFrameType + public let startTime: CMTime public let duration: CMTime public let rotationAngle: Double public let aspect: Double @@ -40,16 +41,17 @@ public final class MediaTrackFrameBuffer { private var frameSourceSinkIndex: Int? - private var frames: [MediaTrackDecodableFrame] = [] + private(set) var frames: [MediaTrackDecodableFrame] = [] private var maxFrameTime: Double? private var endOfStream = false private var bufferedUntilTime: CMTime? private var isWaitingForLowWaterDuration: Bool = false - init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, duration: CMTime, rotationAngle: Double, aspect: Double, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { + init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, startTime: CMTime, duration: CMTime, rotationAngle: Double, aspect: Double, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { self.frameSource = frameSource self.type = type self.decoder = decoder + self.startTime = startTime self.duration = duration self.rotationAngle = rotationAngle self.aspect = aspect @@ -192,8 +194,10 @@ public final class MediaTrackFrameBuffer { if self.endOfStream, let decodedFrame = self.decoder.takeRemainingFrame() { return .frame(decodedFrame) } else { - if let bufferedUntilTime = self.bufferedUntilTime { - if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 || self.endOfStream { + if self.endOfStream { + return .finished + } else if let bufferedUntilTime = self.bufferedUntilTime { + if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 { return .finished } } diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 0ac0e1c56c6..8d919864c69 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -38,15 +38,17 @@ private final class SoftwareVideoStream { let index: Int let fps: CMTime let timebase: CMTime + let startTime: CMTime let duration: CMTime let decoder: FFMpegMediaVideoFrameDecoder let rotationAngle: Double let aspect: Double - init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { + init(index: Int, fps: CMTime, timebase: CMTime, startTime: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { self.index = index self.fps = fps self.timebase = timebase + self.startTime = startTime self.duration = duration self.decoder = decoder self.rotationAngle = rotationAngle @@ -126,6 +128,13 @@ public final class SoftwareVideoSource { let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) let metrics = avFormatContext.metricsForStream(at: streamIndex) @@ -137,7 +146,7 @@ public final class SoftwareVideoSource { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { - videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } } @@ -222,6 +231,13 @@ public final class SoftwareVideoSource { } } + public func readTrackInfo() -> (offset: CMTime, duration: CMTime)? { + guard let videoStream = self.videoStream else { + return nil + } + return (videoStream.startTime, CMTimeMaximum(CMTime(value: 0, timescale: videoStream.duration.timescale), CMTimeSubtract(videoStream.duration, videoStream.startTime))) + } + public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else { return (nil, 0.0, 1.0, false) @@ -490,3 +506,113 @@ public final class SoftwareAudioSource { } } } + +public final class FFMpegMediaInfo { + public let startTime: CMTime + public let duration: CMTime + + public init(startTime: CMTime, duration: CMTime) { + self.startTime = startTime + self.duration = duration + } +} + +private final class FFMpegMediaInfoExtractContext { + let fd: Int32 + let size: Int + + init(fd: Int32, size: Int) { + self.fd = fd + self.size = size + } +} + +private func FFMpegMediaInfoExtractContextReadPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + let result = read(context.fd, buffer, Int(bufferSize)) + if result == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + return Int32(result) +} + +private func FFMpegMediaInfoExtractContextSeekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if (whence & FFMPEG_AVSEEK_SIZE) != 0 { + return Int64(context.size) + } else { + lseek(context.fd, off_t(offset), SEEK_SET) + return offset + } +} + +public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? { + let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + + var s = stat() + stat(path, &s) + let size = Int32(s.st_size) + + let fd = open(path, O_RDONLY, S_IRUSR) + if fd < 0 { + return nil + } + defer { + close(fd) + } + + let avFormatContext = FFMpegAVFormatContext() + let ioBufferSize = 64 * 1024 + + let context = FFMpegMediaInfoExtractContext(fd: fd, size: Int(size)) + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(context).toOpaque(), readPacket: FFMpegMediaInfoExtractContextReadPacketCallback, writePacket: nil, seek: FFMpegMediaInfoExtractContextSeekCallback, isSeekable: true) else { + return nil + } + + avFormatContext.setIO(avIoContext) + + if !avFormatContext.openInput() { + return nil + } + + if !avFormatContext.findStreamInfo() { + return nil + } + + var streamInfos: [(isVideo: Bool, info: FFMpegMediaInfo)] = [] + + for typeIndex in 0 ..< 1 { + let isVideo = typeIndex == 0 + + for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) { + let streamIndex = streamIndexNumber.int32Value + if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { + continue + } + + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (_, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) + duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime)) + + streamInfos.append((isVideo: isVideo, info: FFMpegMediaInfo(startTime: startTime, duration: duration))) + } + } + + if let video = streamInfos.first(where: \.isVideo) { + return video.info + } else if let stream = streamInfos.first { + return stream.info + } else { + return nil + } +} diff --git a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift index 58109e5febd..a94c48e3263 100644 --- a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift +++ b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift @@ -15,8 +15,7 @@ public func preloadVideoResource(postbox: Postbox, userLocation: MediaResourceUs let disposable = MetaDisposable() queue.async { let maximumFetchSize = 2 * 1024 * 1024 + 128 * 1024 - //let maximumFetchSize = 128 - let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, streamable: true, isSeekable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) + let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, limitedFileRange: nil, streamable: true, isSeekable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) let source = QueueLocalObject(queue: queue, generate: { return sourceImpl }) diff --git a/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift b/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift index 40ba6290e96..6e6196cd11d 100644 --- a/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift +++ b/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift @@ -114,4 +114,12 @@ final class VideoPlayerProxy { nodeRef.release() } } + + func flush() { + self.withContext { context in + if let context = context { + context.node?.reset() + } + } + } } diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index 1b9491b9396..399acc50975 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -534,6 +534,14 @@ - (NSString *)platformString return @"iPhone 15 Pro"; if ([platform isEqualToString:@"iPhone16,2"]) return @"iPhone 15 Pro Max"; + if ([platform isEqualToString:@"iPhone17,3"]) + return @"iPhone 16"; + if ([platform isEqualToString:@"iPhone17,4"]) + return @"iPhone 16 Plus"; + if ([platform isEqualToString:@"iPhone17,1"]) + return @"iPhone 16 Pro"; + if ([platform isEqualToString:@"iPhone17,2"]) + return @"iPhone 16 Pro Max"; if ([platform hasPrefix:@"iPod1"]) return @"iPod touch 1G"; diff --git a/submodules/MtProtoKit/Sources/PingFoundation.h b/submodules/MtProtoKit/Sources/PingFoundation.h index ffb54db2757..07041a0c7b0 100644 --- a/submodules/MtProtoKit/Sources/PingFoundation.h +++ b/submodules/MtProtoKit/Sources/PingFoundation.h @@ -59,7 +59,7 @@ typedef NS_ENUM(NSInteger, PingFoundationAddressStyle) { /*! The address family for `hostAddress`, or `AF_UNSPEC` if that's nil. */ -@property (nonatomic, assign, readonly) sa_family_t hostAddressFamily; +@property (nonatomic, assign, readonly) __uint8_t hostAddressFamily; /*! The identifier used by pings by this object. * \details When you create an instance of this object it generates a random identifier diff --git a/submodules/MtProtoKit/Sources/PingFoundation.m b/submodules/MtProtoKit/Sources/PingFoundation.m index f605c272e66..dfb6df3924c 100644 --- a/submodules/MtProtoKit/Sources/PingFoundation.m +++ b/submodules/MtProtoKit/Sources/PingFoundation.m @@ -138,8 +138,8 @@ - (void)dealloc [self stop]; } -- (sa_family_t)hostAddressFamily { - sa_family_t result; +- (__uint8_t)hostAddressFamily { + __uint8_t result; result = AF_UNSPEC; if ( (self.hostAddress != nil) && (self.hostAddress.length >= sizeof(struct sockaddr)) ) diff --git a/submodules/OpenSSLEncryptionProvider/Package.swift b/submodules/OpenSSLEncryptionProvider/Package.swift index 1fd16683193..a8c920d7c80 100644 --- a/submodules/OpenSSLEncryptionProvider/Package.swift +++ b/submodules/OpenSSLEncryptionProvider/Package.swift @@ -23,10 +23,8 @@ let package = Package( publicHeadersPath: "PublicHeaders", cSettings: [ .headerSearchPath("PublicHeaders"), - .unsafeFlags([ - "-I../../../../core-xprojects/openssl/build/openssl/include", - "-I../EncryptionProvider/PublicHeaders" - ]) + .headerSearchPath("SharedHeaders/openssl/include"), + .headerSearchPath("SharedHeaders/EncryptionProvider"), ]), ] ) diff --git a/submodules/OpusBinding/Package.swift b/submodules/OpusBinding/Package.swift index 12028015a3f..045c0e30c35 100644 --- a/submodules/OpusBinding/Package.swift +++ b/submodules/OpusBinding/Package.swift @@ -28,8 +28,8 @@ let package = Package( cSettings: [ .headerSearchPath("PublicHeaders"), .headerSearchPath("PublicHeaders/OpusBinding"), + .headerSearchPath("SharedHeaders/libopus/include"), .headerSearchPath("Sources"), - .unsafeFlags(["-I../../../../core-xprojects/libopus/build/libopus/include"]) ]), ] ) diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 5819862754c..c5032937c6b 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 37b5bc76e64..759746fba5f 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -187,7 +187,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let subject: ShareControllerSubject var actionCompletionText: String? if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) subject = .media(videoFileReference.abstract) actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved } else { @@ -279,9 +279,9 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { if video != previousVideoRepresentations?.last { let mediaManager = self.context.sharedContext.mediaManager - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true self.videoStartTimestamp = video.representation.startTimestamp diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 36d3ff73aae..8361ff8897f 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -370,7 +370,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.canAttachContent = true videoNode.isHidden = true @@ -519,7 +519,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { self.isReady.set(.single(true)) } } else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { @@ -1135,17 +1135,22 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { let subject: ShareControllerSubject var actionCompletionText: String? if let video = videoRepresentations.last, let peer = self.peer, let peerReference = PeerReference(peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) subject = .media(videoFileReference.abstract) actionCompletionText = presentationData.strings.Gallery_VideoSaved } else { subject = .image(representations) actionCompletionText = presentationData.strings.Gallery_ImageSaved } - let shareController = ShareController(context: self.context, subject: subject, preferredAction: .saveToCameraRoll) + + var forceTheme: PresentationTheme? + if !presentationData.theme.overallDarkAppearance { + forceTheme = defaultDarkColorPresentationTheme + } + + let shareController = ShareController(context: self.context, subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme) shareController.actionCompleted = { [weak self] in if let self = self, let actionCompletionText = actionCompletionText { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.presentController?(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true })) } } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 39f2a74a066..b0173828c72 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -448,14 +448,22 @@ private func chatMessageImageFileThumbnailDatas(account: Account, userLocation: return signal } -private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { +private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { let fullSizeResource = fileReference.media.resource var reducedSizeResource: MediaResource? - if let videoThumbnail = fileReference.media.videoThumbnails.first { + if let previewSourceFileReference, let videoThumbnail = previewSourceFileReference.media.videoThumbnails.first { + reducedSizeResource = videoThumbnail.resource + } else if let videoThumbnail = fileReference.media.videoThumbnails.first { reducedSizeResource = videoThumbnail.resource } - let thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations) + var thumbnailRepresentation: TelegramMediaImageRepresentation? + if let previewSourceFileReference { + thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(previewSourceFileReference.media.previewRepresentations) : smallestImageRepresentation(previewSourceFileReference.media.previewRepresentations) + } + if thumbnailRepresentation == nil { + thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations) + } let thumbnailResource = thumbnailRepresentation?.resource let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) @@ -979,7 +987,7 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou } public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false, synchronousLoads: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) + let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, previewSourceFileReference: nil, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) return signal |> map { value in @@ -1581,7 +1589,7 @@ public func mediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceU } } -public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { +public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad, forceThumbnail: blurred) @@ -1592,7 +1600,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaR return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete) } } else { - signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) + signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, previewSourceFileReference: previewSourceFileReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) } return signal @@ -3146,3 +3154,20 @@ public func callDefaultBackground() -> Signal<(TransformImageArguments) -> Drawi return context }) } + +public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return .single({ arguments in + guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { + return nil + } + + context.withFlippedContext { c in + c.setFillColor(color.withAlphaComponent(1.0).cgColor) + c.fill(arguments.drawingRect) + } + + addCorners(context, arguments: arguments) + + return context + }) +} diff --git a/submodules/Postbox/Sources/FileSize.swift b/submodules/Postbox/Sources/FileSize.swift index 3740d7b142d..ef4fc4863a3 100644 --- a/submodules/Postbox/Sources/FileSize.swift +++ b/submodules/Postbox/Sources/FileSize.swift @@ -1,19 +1,27 @@ import Foundation public func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { - if useTotalFileAllocatedSize { + /*if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } } - } + }*/ var value = stat() - if stat(path, &value) == 0 { + if lstat(path, &value) == 0 { + if (value.st_mode & S_IFMT) == S_IFLNK { + return 0 + } + + if useTotalFileAllocatedSize { + return Int64(value.st_blocks) * Int64(value.st_blksize) + } + return value.st_size } else { return nil diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 066afb81ec2..4c0e3e28eb3 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -101,6 +101,18 @@ public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { return true } +public func areMediaArraysSemanticallyEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if !lhs[i].isSemanticallyEqual(to: rhs[i]) { + return false + } + } + return true +} + public func areMediaDictionariesEqual(_ lhs: [MediaId: Media], _ rhs: [MediaId: Media]) -> Bool { if lhs.count != rhs.count { return false diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 97e2c893b68..bd9e1d34554 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -140,8 +140,8 @@ public final class MediaBox { private let statusQueue = Queue() private let concurrentQueue = Queue.concurrentDefaultQueue() - private let dataQueue = Queue(name: "MediaBox-Data") - private let dataFileManager: MediaBoxFileManager + public let dataQueue = Queue(name: "MediaBox-Data") + public let dataFileManager: MediaBoxFileManager private let cacheQueue = Queue() private let timeBasedCleanup: TimeBasedCleanup @@ -209,60 +209,6 @@ public final class MediaBox { self.dataFileManager = MediaBoxFileManager(queue: self.dataQueue) let _ = self.ensureDirectoryCreated - - //self.updateResourceIndex() - - /*#if DEBUG - self.dataQueue.async { - for _ in 0 ..< 5 { - let tempFile = TempBox.shared.tempFile(fileName: "file") - print("MediaBox test: file \(tempFile.path)") - let queue2 = Queue.concurrentDefaultQueue() - if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: tempFile.path.data(using: .utf8)!, path: tempFile.path + "_complete", partialPath: tempFile.path + "_partial", metaPath: tempFile.path + "_partial" + ".meta") { - let _ = fileContext.fetched( - range: 0 ..< Int64.max, - priority: .default, - fetch: { ranges in - return ranges - |> filter { !$0.isEmpty } - |> take(1) - |> castError(MediaResourceDataFetchError.self) - |> mapToSignal { _ in - return Signal { subscriber in - queue2.async { - subscriber.putNext(.resourceSizeUpdated(524288)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(393216)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(655360)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(169608)) - } - queue2.async { - subscriber.putNext(.dataPart(resourceOffset: 131072, data: Data(repeating: 0xbb, count: 38536), range: 0 ..< 38536, complete: true)) - } - queue2.async { - subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(repeating: 0xaa, count: 131072), range: 0 ..< 131072, complete: false)) - } - - return EmptyDisposable - } - } - }, - error: { _ in - }, - completed: { - assert(try! Data(contentsOf: URL(fileURLWithPath: tempFile.path + "_complete")) == Data(repeating: 0xaa, count: 131072) + Data(repeating: 0xbb, count: 38536)) - let _ = fileContext.addReference() - } - ) - } - } - } - #endif*/ } public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { @@ -641,21 +587,12 @@ public final class MediaBox { paths.partial + ".meta" ]) - #if true if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { context = fileContext self.fileContexts[resourceId] = fileContext } else { return nil } - #else - if let fileContext = MediaBoxFileContextImpl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { - context = fileContext - self.fileContexts[resourceId] = fileContext - } else { - return nil - } - #endif } if let context = context { let index = context.addReference() @@ -737,6 +674,28 @@ public final class MediaBox { return self.resourceData(id: resource.id, size: size, in: range, mode: mode, notifyAboutIncomplete: notifyAboutIncomplete, attemptSynchronously: attemptSynchronously) } + public func internal_resourceData(id: MediaResourceId, size: Int64, in range: Range) -> (file: ManagedFile, length: Int)? { + let paths = self.storePathsForId(id) + + self.timeBasedCleanup.touch(paths: [ + paths.complete + ]) + + if let file = ManagedFile(queue: nil, path: paths.complete, mode: .read), let completeSize = file.getSize() { + let clippedLowerBound = min(completeSize, max(0, range.lowerBound)) + let clippedUpperBound = min(completeSize, max(0, range.upperBound)) + if clippedLowerBound < clippedUpperBound && (clippedUpperBound - clippedLowerBound) <= 64 * 1024 * 1024 { + let _ = file.seek(position: clippedLowerBound) + return (file, Int(clippedUpperBound - clippedLowerBound)) + } else { + return nil + } + } else { + let tempManager = MediaBoxFileManager(queue: nil) + return MediaBoxPartialFile.internal_extractPartialData(manager: tempManager, path: paths.partial, metaPath: paths.partial + ".meta", range: range) + } + } + public func resourceData(id: MediaResourceId, size: Int64, in range: Range, mode: ResourceDataRangeMode = .complete, notifyAboutIncomplete: Bool = false, attemptSynchronously: Bool = false) -> Signal<(Data, Bool), NoError> { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 44472bd1ce6..8e1d9d6d0cb 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -92,6 +92,20 @@ final class MediaBoxPartialFile { return fd.readData(count: Int(clippedRange.upperBound - clippedRange.lowerBound)) } + static func internal_extractPartialData(manager: MediaBoxFileManager, path: String, metaPath: String, range: Range) -> (file: ManagedFile, length: Int)? { + guard let fd = ManagedFile(queue: nil, path: path, mode: .read) else { + return nil + } + guard let fileMap = try? MediaBoxFileMap.read(manager: manager, path: metaPath) else { + return nil + } + guard let clippedRange = fileMap.contains(range) else { + return nil + } + let _ = fd.seek(position: Int64(clippedRange.lowerBound)) + return (fd, Int(clippedRange.upperBound - clippedRange.lowerBound)) + } + var storedSize: Int64 { assert(self.queue.isCurrent()) return self.fileMap.sum diff --git a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift index 6bc400325ab..3d1e70b8895 100644 --- a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift +++ b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift @@ -2,7 +2,7 @@ import Foundation import RangeSet import SwiftSignalKit -final class MediaBoxFileContextV2Impl: MediaBoxFileContext { +public final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private final class RangeRequest { let value: Range let priority: MediaBoxFetchPriority @@ -99,7 +99,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private final class PartialState { private let queue: Queue private let manager: MediaBoxFileManager - private let storageBox: StorageBox + private let storageBox: StorageBox? private let resourceId: Data private let partialPath: String private let fullPath: String @@ -124,7 +124,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { init( queue: Queue, manager: MediaBoxFileManager, - storageBox: StorageBox, + storageBox: StorageBox?, resourceId: Data, partialPath: String, fullPath: String, @@ -461,7 +461,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { self.fileMap.fill(range) self.fileMap.serialize(manager: self.manager, to: self.metaPath) - self.storageBox.update(id: self.resourceId, size: self.fileMap.sum) + self.storageBox?.update(id: self.resourceId, size: self.fileMap.sum) } else { postboxLog("MediaBoxFileContextV2Impl: error seeking file to \(resourceOffset) at \(self.partialPath)") } @@ -474,7 +474,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private func processMovedFile() { if let size = fileSize(self.fullPath) { self.isComplete = true - self.storageBox.update(id: self.resourceId, size: size) + self.storageBox?.update(id: self.resourceId, size: size) } } @@ -623,7 +623,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private let queue: Queue private let manager: MediaBoxFileManager - private let storageBox: StorageBox + private let storageBox: StorageBox? private let resourceId: Data private let path: String private let partialPath: String @@ -637,10 +637,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { return self.references.isEmpty } - init?( + public init?( queue: Queue, manager: MediaBoxFileManager, - storageBox: StorageBox, + storageBox: StorageBox?, resourceId: Data, path: String, partialPath: String, @@ -683,7 +683,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { + public func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { assert(self.queue.isCurrent()) if let size = fileSize(self.path) { @@ -708,7 +708,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func fetched( + public func fetched( range: Range, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal, @@ -734,7 +734,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func fetchedFullRange( + public func fetchedFullRange( fetch: @escaping (Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void @@ -758,7 +758,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func cancelFullRangeFetches() { + public func cancelFullRangeFetches() { assert(self.queue.isCurrent()) if let partialState = self.partialState { @@ -766,7 +766,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func rangeStatus(next: @escaping (RangeSet) -> Void, completed: @escaping () -> Void) -> Disposable { + public func rangeStatus(next: @escaping (RangeSet) -> Void, completed: @escaping () -> Void) -> Disposable { assert(self.queue.isCurrent()) if let size = fileSize(self.path) { @@ -781,7 +781,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable { + public func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable { assert(self.queue.isCurrent()) if let _ = fileSize(self.path) { diff --git a/submodules/Postbox/Sources/MediaBoxFileManager.swift b/submodules/Postbox/Sources/MediaBoxFileManager.swift index bc963b8e4cf..22799bc0a5d 100644 --- a/submodules/Postbox/Sources/MediaBoxFileManager.swift +++ b/submodules/Postbox/Sources/MediaBoxFileManager.swift @@ -2,13 +2,13 @@ import Foundation import SwiftSignalKit import ManagedFile -final class MediaBoxFileManager { - enum Mode { +public final class MediaBoxFileManager { + public enum Mode { case read case readwrite } - enum AccessError: Error { + public enum AccessError: Error { case generic } @@ -129,7 +129,7 @@ final class MediaBoxFileManager { private var nextItemId: Int = 0 private let maxOpenFiles: Int - init(queue: Queue?) { + public init(queue: Queue?) { self.queue = queue self.maxOpenFiles = 16 } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index ff73495535c..339c9da479c 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -18,6 +18,8 @@ private func md5Hash(_ data: Data) -> HashId { return HashId(data: hashData) } +public var storageBoxDebugFunc: ((Int64) -> Void)? + public final class StorageBox { public final class Stats { public final class ContentTypeStats { @@ -320,7 +322,11 @@ public final class StorageBox { currentSize = 0 } - self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + withExtendedLifetime(key, { + withUnsafeMutablePointer(to: ¤tSize, { pointer in + self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: UnsafeMutableRawPointer(pointer), capacity: 8, length: 8, freeWhenDone: false)) + }) + }) self.totalSize += delta } @@ -342,7 +348,11 @@ public final class StorageBox { currentSize = 0 } - self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + withExtendedLifetime(key, { + withUnsafeMutablePointer(to: ¤tSize, { pointer in + self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: UnsafeMutableRawPointer(pointer), capacity: 8, length: 8, freeWhenDone: false)) + }) + }) } func internalAdd(reference: Reference, to id: Data, contentType: UInt8, size: Int64?) { diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift index 365aa0e83d1..874fac42f4d 100644 --- a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift @@ -119,7 +119,7 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { } else if value is String { try self.encode(value as! String) } else if value is Data { - try self.encode(value as! Data) + try self.encodeData(value as! Data) } else if let value = value as? AdaptedPostboxEncoder.RawObjectData { let buffer = WriteBuffer() @@ -159,10 +159,6 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { func encode(_ value: String) throws { self.items.append(.string(value)) } - - func encode(_ value: Data) throws { - self.items.append(.data(value)) - } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { preconditionFailure() @@ -177,6 +173,12 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { } } +private extension _AdaptedPostboxEncoder.UnkeyedContainer { + func encodeData(_ value: Data) throws { + self.items.append(.data(value)) + } +} + extension _AdaptedPostboxEncoder.UnkeyedContainer: AdaptedPostboxEncodingContainer { func makeData() -> Data { preconditionFailure() diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 3e6c32d5b05..e77b52af644 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -784,7 +784,6 @@ private func createGiveawayControllerEntries( entries.append(.prepaid(presentationData.theme, title, text, prepaidGiveaway)) } - var starsPerUser: Int64 = 0 if case .generic = subject, case .starsGiveaway = state.mode, !starsGiveawayOptions.isEmpty { let selectedOption = starsGiveawayOptions.first(where: { $0.giveawayOption.count == state.stars })! entries.append(.starsHeader(presentationData.theme, presentationData.strings.BoostGift_Stars_Title.uppercased(), presentationData.strings.BoostGift_Stars_Boosts(selectedOption.giveawayOption.yearlyBoosts).uppercased())) @@ -795,13 +794,16 @@ private func createGiveawayControllerEntries( continue } let giftTitle: String = presentationData.strings.BoostGift_Stars_Stars(Int32(product.giveawayOption.count)) - let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) ?? product.giveawayOption.winners.first! - let maxWinners = product.giveawayOption.winners.sorted(by: { $0.users < $1.users }).last?.users ?? 1 - - let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(winners.starsPerUser)").string + + let starsPerUser: Int64 + if let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) { + starsPerUser = winners.starsPerUser + } else { + starsPerUser = product.giveawayOption.count / Int64(state.winners) + } + let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(starsPerUser)").string let label = product.storeProduct.price - starsPerUser = winners.starsPerUser let isSelected = product.giveawayOption.count == state.stars entries.append(.stars(i, presentationData.theme, Int32(product.giveawayOption.count), giftTitle, subtitle, label, isSelected, maxWinners)) @@ -941,7 +943,6 @@ private func createGiveawayControllerEntries( if state.mode == .starsGiveaway { let starsString = presentationData.strings.BoostGift_AdditionalPrizesInfoStars(Int32(state.stars)) if state.prizeDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let _ = starsPerUser prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, "").string } else { prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, presentationData.strings.BoostGift_AdditionalPrizesInfoStarsAndOther("\(state.winners)", state.prizeDescription).string).string @@ -1356,7 +1357,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio return } let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount - purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(state.peers.count) storeProduct = selectedProduct.storeProduct case .starsGiveaway: diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 34b2ff7a8e1..8b963e33369 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -233,7 +233,7 @@ private final class PhoneView: UIView { hintDimensions: CGSize(width: 1170, height: 1754), storeAfterDownload: nil ) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.canAttachContent = true self.videoNode = videoNode @@ -626,7 +626,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -646,9 +646,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -706,8 +706,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -722,7 +722,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 50bea98f1ae..c6d310bcb71 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -1034,7 +1034,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { _ in return nil }, diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index fc876e6e819..51a2d5e99bb 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -645,7 +645,8 @@ private final class DemoSheetContent: CombinedComponent { immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, - attributes: file.attributes + attributes: file.attributes, + alternativeRepresentations: file.alternativeRepresentations ) } default: diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index bc802fe14c3..036c6ce857e 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -625,7 +625,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if let signal = signal { let _ = (signal |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in controller?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -910,7 +910,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first { purpose = .gift(peerId: peerId, currency: currency, amount: amount) } else { - purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) + purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(self.peerIds.count) } diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 3425dff41f8..ec27590dd48 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2576,7 +2576,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { footer: AnyComponent(MultilineTextComponent( text: .plain(adsInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -2739,7 +2740,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if let signal = signal { let _ = (signal |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in controller?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 3f1fa037fd0..77609c80f6e 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -158,7 +158,8 @@ public class PremiumLimitsListScreen: ViewController { immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, - attributes: file.attributes + attributes: file.attributes, + alternativeRepresentations: file.alternativeRepresentations ) } default: diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index 610480dfc73..1fa7f48d18a 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -5,11 +5,11 @@ import AccountContext import SwiftSignalKit import TelegramPresentationData -public func textAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { - return textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) +public func textAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + return textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) } -public func textAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { +public func textAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { var presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } if let forceTheme = forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -21,7 +21,7 @@ public func textAlertController(sharedContext: SharedAccountContext, updatedPres presentationData = presentationData.withUpdated(theme: forceTheme) } return AlertControllerTheme(presentationData: presentationData) - }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) + }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) } public func textAlertController(sharedContext: SharedAccountContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissOnOutsideTap: Bool = true) -> AlertController { diff --git a/submodules/PromptUI/BUILD b/submodules/PromptUI/BUILD index 79a9b751022..3b02aac522d 100644 --- a/submodules/PromptUI/BUILD +++ b/submodules/PromptUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramStringFormatting", ], visibility = [ "//visibility:public", diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index e6e9e19889f..46e58a30abe 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -7,6 +7,7 @@ import Postbox import TelegramCore import TelegramPresentationData import AccountContext +import TelegramStringFormatting private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private var theme: PresentationTheme @@ -39,7 +40,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg } } - init(theme: PresentationTheme, placeholder: String, characterLimit: Int) { + init(theme: PresentationTheme, placeholder: String, characterLimit: Int, isPassword: Bool = false) { self.theme = theme self.characterLimit = characterLimit @@ -60,6 +61,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg self.textInputNode.returnKeyType = .done self.textInputNode.autocorrectionType = .default self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + self.textInputNode.isSecureTextEntry = isPassword self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -448,3 +450,538 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent } return controller } + +private final class AuthAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + + private let title: String + private let text: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + let inputFieldNode: PromptInputFieldNode + let passwordFieldNode: PromptInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) { + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 2 + + self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "User Name", characterLimit: 1024) + self.passwordFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Password", characterLimit: 1024, isPassword: true) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.inputFieldNode) + self.addSubnode(self.passwordFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = true + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var login: String { + return self.inputFieldNode.text + } + + var password: String { + return self.passwordFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + let spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let fieldSpacing: CGFloat = -11.0 + origin.y += inputFieldHeight + fieldSpacing + + let passwordFieldHeight = self.passwordFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + transition.updateFrame(node: self.passwordFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: passwordFieldHeight)) + + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + fieldSpacing + passwordFieldHeight + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +public func authController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, title: String, text: String, apply: @escaping ((String, String)?) -> Void) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + apply(nil) + }), TextAlertAction(type: .defaultAction, title: "Sign In", action: { + dismissImpl?(true) + applyImpl?() + })] + + let contentNode = AuthAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + apply((contentNode.login, contentNode.password)) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} + + +private final class ProgressAlertContentNode: AlertContentNode { + private let theme: AlertControllerTheme + private let strings: PresentationStrings + + private let title: String + var text: String { + didSet { + self.updateTheme(self.theme) + } + } + + private let titleNode: ASTextNode + private let textNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) { + self.theme = theme + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 2 + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = true + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + let spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +public func progressAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, title: String, cancel: @escaping () -> Void) -> (AlertController, (Int64, Int64) -> Void) { + let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Cancel", action: { + dismissImpl?(true) + applyImpl?() + })] + + let contentNode = ProgressAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: "Downloading...", text: " ") + + let updateProgress: (Int64, Int64) -> Void = { [weak contentNode] current, total in + if let contentNode { + contentNode.text = "\(dataSizeString(Int(current), formatting: DataSizeStringFormatting(presentationData: presentationData))) of \(dataSizeString(Int(total), formatting: DataSizeStringFormatting(presentationData: presentationData)))" + } + } + applyImpl = { + dismissImpl?(true) + cancel() + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + + return (controller, updateProgress) +} diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 3b8370da8b9..21cc6684a03 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -899,7 +899,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie guard let navigationController = self.controller?.navigationController as? NavigationController else { return false } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let strongSelf = self else { return } diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index a32b6c972e6..0e3a0301aed 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -238,6 +238,18 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal, _ s26: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26) + }, initialValues: [:], queue: queue) +} + +public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal, _ s26: Signal, _ s27: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26), signalOfAny(s27)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26, values[26] as! T27) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 312ebb57a88..c039babd7d1 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -177,7 +177,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 8ab3cdc71c9..2caf610c6a5 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -656,7 +656,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak)) entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo)) - if let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent { + if !"".isEmpty, let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent { entries.append(.sensitiveContent(presentationData.strings.Settings_SensitiveContent, contentSettingsConfiguration.sensitiveContentEnabled)) entries.append(.sensitiveContentInfo(presentationData.strings.Settings_SensitiveContentInfo)) } diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index f9a67fe0970..077f89f65de 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -310,7 +310,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -351,7 +351,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift index 1736b737a06..88351caec74 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -82,11 +82,7 @@ private func translationSettingsControllerEntries(theme: PresentationTheme, stri if activeLanguage.hasSuffix(rawSuffix) { activeLanguage = String(activeLanguage.dropLast(rawSuffix.count)) } - if activeLanguage == "nb" { - activeLanguage = "no" - } else if activeLanguage == "pt-br" { - activeLanguage = "pt" - } + activeLanguage = normalizeTranslationLanguage(activeLanguage) selectedLanguages = Set([activeLanguage]) for language in systemLanguageCodes() { selectedLanguages.insert(language) diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 67aa3a7444b..b0f268b676a 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -217,7 +217,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 14baaadd015..9df2804a252 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -1068,7 +1068,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let faq = SettingsSearchableItem(id: .faq(0), title: strings.Settings_FAQ, alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in let _ = (cachedFaqInstantPage(context: context) |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in present(.push, controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 347f115d99e..2f6c753e4a8 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { @@ -450,7 +450,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index c8030edd2e2..0b009def439 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -538,7 +538,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get()) |> take(1)).start(next: { previewTheme, settings in let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")], alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start() diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 974bf7976fc..1d953a2db46 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { @@ -615,7 +615,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) diff --git a/submodules/ShareController/Sources/Nicegram/ShareController+Nicegram.swift b/submodules/ShareController/Sources/Nicegram/ShareController+Nicegram.swift new file mode 100644 index 00000000000..def44c6141c --- /dev/null +++ b/submodules/ShareController/Sources/Nicegram/ShareController+Nicegram.swift @@ -0,0 +1,63 @@ +import AccountContext +import TelegramCore +import UIKit + +public func shareController( + image: UIImage?, + text: String, + context: AccountContext +) async -> ShareController { + let media = await prepareImage(image: image, context: context) + + let subject: ShareControllerSubject + if let media { + subject = .media(media, text: text) + } else { + subject = .text(text) + } + + return await ShareController( + context: context, + subject: subject + ) +} + +private func prepareImage( + image: UIImage?, + context: AccountContext +) async -> AnyMediaReference? { + guard let image else { + return nil + } + + let account = context.account + let postbox = account.postbox + let network = account.network + let peerId = account.peerId + + // Copied from + // https://bitbucket.org/mobyrix/nicegram-ios/src/develop/submodules/ShareItems/Sources/ShareItems.swift + let nativeImageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + let dimensions = nativeImageSize.fitted(CGSize(width: 1280.0, height: 1280.0)) + if let scaledImage = scalePhotoImage(image, dimensions: dimensions), + let imageData = scaledImage.jpegData(compressionQuality: 0.52) { + let stream = standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(dimensions)).asyncStream(.bufferingNewest(1)) + for await event in stream { + if case let .result(result) = event, + case let .media(media) = result { + return media + } + } + } + + return nil +} + +private func scalePhotoImage(_ image: UIImage, dimensions: CGSize) -> UIImage? { + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: dimensions, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: dimensions)) + } +} diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 799f962ad19..3b8eaa433a8 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -74,7 +74,8 @@ public enum ShareControllerSubject { case quote(text: String, url: String) case messages([Message]) case image([ImageRepresentationWithReference]) - case media(AnyMediaReference) + // MARK: Nicegram, text added + case media(AnyMediaReference, text: String = "") case mapMedia(TelegramMediaMap) case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) } @@ -572,7 +573,8 @@ public final class ShareController: ViewController { self?.actionCompleted?() }) } - case let .media(mediaReference): + // MARK: Nicegram, text added + case let .media(mediaReference, _): var canSave = false var isVideo = false if mediaReference.media is TelegramMediaImage { @@ -813,7 +815,8 @@ public final class ShareController: ViewController { return false } } - case let .media(mediaReference): + // MARK: Nicegram, text added + case let .media(mediaReference, _): var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -1029,8 +1032,9 @@ public final class ShareController: ViewController { case let .image(representations): let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: .standalone(media: media))) - case let .media(mediaReference): - collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: mediaReference)) + // MARK: Nicegram, text added + case let .media(mediaReference, text): + collectableItems.append(CollectableExternalShareItem(url: "", text: text, author: nil, timestamp: nil, mediaReference: mediaReference)) case let .mapMedia(media): let latLong = "\(media.latitude),\(media.longitude)" collectableItems.append(CollectableExternalShareItem(url: "https://maps.apple.com/maps?ll=\(latLong)&q=\(latLong)&t=m", text: "", author: nil, timestamp: nil, mediaReference: nil)) @@ -1570,7 +1574,10 @@ public final class ShareController: ViewController { messages: messages )) } - case let .media(mediaReference): + // MARK: Nicegram, text added + case let .media(mediaReference, string): + let text = "\(text)\n\n\(string)".trimmingCharacters(in: .whitespacesAndNewlines) + var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true @@ -2093,7 +2100,10 @@ public final class ShareController: ViewController { messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } - case let .media(mediaReference): + // MARK: Nicegram, text added + case let .media(mediaReference, string): + let text = "\(text)\n\n\(string)".trimmingCharacters(in: .whitespacesAndNewlines) + var sendTextAsCaption = false if mediaReference.media is TelegramMediaImage || mediaReference.media is TelegramMediaFile { sendTextAsCaption = true diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 292cc01841b..eb958dece86 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -17,9 +17,9 @@ import AccountContext private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } @@ -279,11 +279,11 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte if let postbox, let mediaManager = environment.mediaManager, 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: EngineMedia.Id(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: [], preloadSize: nil, coverTime: nil)]) + let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(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: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: 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 diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 71ae6be4232..84c74e819c6 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -144,7 +144,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true) let resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: asset.url.path, adjustments: resourceAdjustments) - return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) + return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: nil, videoCodec: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) |> mapError { _ -> PreparedShareItemError in return .generic } @@ -210,7 +210,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let mimeType: String if converted { mimeType = "video/mp4" - attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil), .Animated, .FileName(fileName: "animation.mp4")] + attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil), .Animated, .FileName(fileName: "animation.mp4")] } else { mimeType = "animation/gif" attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")] diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index c6c349766d1..8748c9a40bf 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -55,6 +55,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", + "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 177019657a5..11e14853385 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1167,7 +1167,7 @@ private enum StatsEntry: ItemListNodeEntry { detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } - let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { @@ -1559,8 +1559,13 @@ private func monetizationEntries( ) -> [StatsEntry] { var entries: [StatsEntry] = [] + var isBot = false + if case let .user(user) = peer, let _ = user.botInfo { + isBot = true + } + if canViewRevenue { - entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) + entries.append(.adsHeader(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_Header : presentationData.strings.Monetization_Header)) if !data.topHoursGraph.isEmpty { entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle)) @@ -1602,7 +1607,7 @@ private func monetizationEntries( } if canViewRevenue { - entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) + entries.append(.adsTonBalanceTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_BalanceTitle : presentationData.strings.Monetization_TonBalanceTitle)) entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, data.balances.withdrawEnabled)) if isCreator { @@ -1644,7 +1649,7 @@ private func monetizationEntries( if displayTonTransactions { if !addedTransactionsTabs { - entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased())) + entries.append(.adsTransactionsTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_TransactionsTitle.uppercased() : presentationData.strings.Monetization_TonTransactions.uppercased())) } var transactions = transactionsInfo.transactions @@ -1788,7 +1793,15 @@ private func channelStatsControllerEntries( return [] } -public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { +public func channelStatsController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peerId: PeerId, + section: ChannelStatsSection = .stats, + existingRevenueContext: RevenueStatsContext? = nil, + boostStatus: ChannelBoostStatus? = nil, + boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil +) -> ViewController { let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in @@ -1845,7 +1858,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) - let revenueContext = RevenueStatsContext(account: context.account, peerId: peerId) + let revenueContext = existingRevenueContext ?? RevenueStatsContext(account: context.account, peerId: peerId) let revenueState = Promise() revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) @@ -2013,7 +2026,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD buyAdsImpl?() }, openMonetizationIntro: { - let controller = MonetizationIntroScreen(context: context, openMore: {}) + let controller = MonetizationIntroScreen(context: context, mode: existingRevenueContext != nil ? .bot : .channel, openMore: {}) pushImpl?(controller) }, openMonetizationInfo: { @@ -2112,7 +2125,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD ) |> deliverOnMainQueue |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in - let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData + let (canViewStats, adsRestricted, _, canViewStarsRevenue) = peerData + var canViewRevenue = peerData.2 let _ = canViewStatsValue.swap(canViewStats) @@ -2167,7 +2181,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var headerItem: BoostHeaderItem? var leftNavigationButton: ItemListNavigationButton? var boostsOnly = false - if section == .boosts { + if existingRevenueContext != nil { + title = .text(presentationData.strings.Stats_TonBotRevenue_Title) + canViewRevenue = true + } else if section == .boosts { title = .text("") let headerTitle = isGroup ? presentationData.strings.GroupBoost_Title : presentationData.strings.ChannelBoost_Title @@ -2500,7 +2517,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD })) } var tooltipScreen: UndoOverlayController? + #if compiler(>=6.0) // Xcode 16 + nonisolated(unsafe) var timer: Foundation.Timer? + #else var timer: Foundation.Timer? + #endif showTimeoutTooltipImpl = { cooldownUntilTimestamp in let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 01839afd734..e92c53bd406 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -176,7 +176,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { var isStars = false if let stats = item.stats as? RevenueStats { - let cryptoValue = formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat) amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { @@ -417,7 +417,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { allowActionWhenDisabled: false, displaysProgress: false, action: { [weak self] in - guard let self, let item = self.item, item.isEnabled else { + guard let self, let item = self.item else { return } item.buyAdsAction?() diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index 31db7b01ed7..2dc7e1687bd 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -20,15 +20,18 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let mode: MonetizationIntroScreen.Mode let openMore: () -> Void let dismiss: () -> Void init( context: AccountContext, + mode: MonetizationIntroScreen.Mode, openMore: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode self.openMore = openMore self.dismiss = dismiss } @@ -37,6 +40,9 @@ private final class SheetContent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } @@ -136,7 +142,7 @@ private final class SheetContent: CombinedComponent { let title = title.update( component: BalancedTextComponent( - text: .plain(NSAttributedString(string: strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)), + text: .plain(NSAttributedString(string: component.mode == .bot ? strings.Monetization_Intro_Bot_Title : strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -157,7 +163,7 @@ private final class SheetContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.Monetization_Intro_Ads_Title, titleColor: textColor, - text: strings.Monetization_Intro_Ads_Text, + text: component.mode == .bot ? strings.Monetization_Intro_Bot_Ads_Text : strings.Monetization_Intro_Ads_Text, textColor: secondaryTextColor, iconName: "Ads/Ads", iconColor: linkColor @@ -254,6 +260,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, + highlightColor: linkColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -341,13 +349,16 @@ private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let mode: MonetizationIntroScreen.Mode let openMore: () -> Void init( context: AccountContext, + mode: MonetizationIntroScreen.Mode, openMore: @escaping () -> Void ) { self.context = context + self.mode = mode self.openMore = openMore } @@ -355,6 +366,9 @@ private final class SheetContainerComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } @@ -373,6 +387,7 @@ private final class SheetContainerComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + mode: context.component.mode, openMore: context.component.openMore, dismiss: { animateOut.invoke(Action { _ in @@ -442,9 +457,15 @@ private final class SheetContainerComponent: CombinedComponent { final class MonetizationIntroScreen: ViewControllerComponentContainer { private let context: AccountContext private var openMore: (() -> Void)? - + + enum Mode: Equatable { + case channel + case bot + } + init( context: AccountContext, + mode: Mode, openMore: @escaping () -> Void ) { self.context = context @@ -454,6 +475,7 @@ final class MonetizationIntroScreen: ViewControllerComponentContainer { context: context, component: SheetContainerComponent( context: context, + mode: mode, openMore: openMore ), navigationBarAppearance: .none, diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 1c8ed37359a..c09d2b238e0 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -262,6 +262,13 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { case .ads: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Title + if let floodskipNumber = item.transaction.floodskipNumber { + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else { + itemSubtitle = nil + } case .unsupported: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title itemSubtitle = nil diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 3a59f660130..168dd5459d1 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -772,7 +772,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -782,7 +782,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -792,7 +792,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -836,7 +836,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -846,7 +846,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -856,7 +856,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index a4be4db91a4..61491adb442 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { switch component.transaction { case let .proceeds(amount, fromDate, toDate): labelColor = theme.list.itemDisclosureActions.constructive.fillColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK @@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent { showPeer = true case let .withdrawal(status, amount, date, provider, _, transactionUrl): labelColor = theme.list.itemDestructiveColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.groupingSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) switch status { @@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent { case let .refund(amount, date, _): labelColor = theme.list.itemDisclosureActions.constructive.fillColor titleString = strings.Monetization_TransactionInfo_Refund - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index f227846ba49..c9935ec6d86 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -389,15 +389,14 @@ private final class StickerPackContainer: ASDisplayNode { return updatedOffset } - - + let ignoreCache = controller?.ignoreCache ?? false let fetchedStickerPacks: Signal<[LoadedStickerPack], NoError> = combineLatest(stickerPacks.map { packReference in for pack in loadedStickerPacks { if case let .result(info, _, _) = pack, case let .id(id, _) = packReference, info.id.id == id { return .single(pack) } } - return context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: true) + return context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: true, ignoreCache: ignoreCache) }) self.itemsDisposable = combineLatest(queue: Queue.mainQueue(), fetchedStickerPacks, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))).start(next: { [weak self] contents, peer in @@ -2686,13 +2685,14 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { private var animatedIn: Bool = false fileprivate var initialIsEditing: Bool = false fileprivate var expandIfNeeded: Bool = false + fileprivate let ignoreCache: Bool let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let mainActionTitle: String? let actionTitle: String? - + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -2703,6 +2703,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { actionTitle: String? = nil, isEditing: Bool = false, expandIfNeeded: Bool = false, + ignoreCache: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, @@ -2718,6 +2719,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { self.actionTitle = actionTitle self.initialIsEditing = isEditing self.expandIfNeeded = expandIfNeeded + self.ignoreCache = ignoreCache self.parentNavigationController = parentNavigationController self.sendSticker = sendSticker self.sendEmoji = sendEmoji @@ -2952,6 +2954,7 @@ public func StickerPackScreen( actionTitle: String? = nil, isEditing: Bool = false, expandIfNeeded: Bool = false, + ignoreCache: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? = nil, @@ -2969,6 +2972,7 @@ public func StickerPackScreen( actionTitle: actionTitle, isEditing: isEditing, expandIfNeeded: expandIfNeeded, + ignoreCache: ignoreCache, parentNavigationController: parentNavigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index f7a70061581..4c2568c2acb 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -18,14 +18,28 @@ final class TabBarControllerNode: ASDisplayNode { private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void - var currentControllerNode: ASDisplayNode? { - didSet { - oldValue?.removeFromSupernode() - - if let currentControllerNode = self.currentControllerNode { + var currentControllerNode: ASDisplayNode? + + func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void { + guard node !== self.currentControllerNode else { + return {} + } + + let previousNode = self.currentControllerNode + self.currentControllerNode = node + if let currentControllerNode = self.currentControllerNode { + if let previousNode { + self.insertSubnode(currentControllerNode, aboveSubnode: previousNode) + } else { self.insertSubnode(currentControllerNode, at: 0) } } + + return { [weak self, weak previousNode] in + if previousNode !== self?.currentControllerNode { + previousNode?.removeFromSupernode() + } + } } // MARK: Nicegram (showTabNames) diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index fa2562dca0f..ee7a4b3ec48 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -117,10 +117,10 @@ open class TabBarControllerImpl: ViewController, TabBarController { } } set(value) { let index = max(0, min(self.controllers.count - 1, value)) - if _selectedIndex != index { - _selectedIndex = index + if self._selectedIndex != index { + self._selectedIndex = index - self.updateSelectedIndex() + self.updateSelectedIndex(animated: true) } } } @@ -361,15 +361,20 @@ open class TabBarControllerImpl: ViewController, TabBarController { public func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { let alpha = max(0.0, min(1.0, alpha)) - transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.backgroundNode, alpha: alpha, delay: 0.15) - transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.separatorNode, alpha: alpha, delay: 0.15) + transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.backgroundNode, alpha: alpha, delay: 0.1) + transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.separatorNode, alpha: alpha, delay: 0.1) } - private func updateSelectedIndex() { + private func updateSelectedIndex(animated: Bool = false) { if !self.isNodeLoaded { return } + var animated = animated + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + animated = false + } + var tabBarSelectedIndex = self.selectedIndex if let (cameraItem, _) = self.cameraItemAndAction { if let cameraItemIndex = self.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) { @@ -380,9 +385,21 @@ open class TabBarControllerImpl: ViewController, TabBarController { } self.tabBarControllerNode.tabBarNode.selectedIndex = tabBarSelectedIndex + var transitionSale: CGFloat = 0.998 + if let currentView = self.currentController?.view { + transitionSale = (currentView.frame.height - 3.0) / currentView.frame.height + } if let currentController = self.currentController { currentController.willMove(toParent: nil) - self.tabBarControllerNode.currentControllerNode = nil + //self.tabBarControllerNode.currentControllerNode = nil + + if animated { + currentController.view.layer.animateScale(from: 1.0, to: transitionSale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in + if completed { + currentController.view.layer.removeAllAnimations() + } + }) + } currentController.removeFromParent() currentController.didMove(toParent: nil) @@ -395,13 +412,25 @@ open class TabBarControllerImpl: ViewController, TabBarController { if let currentController = self.currentController { currentController.willMove(toParent: self) - self.tabBarControllerNode.currentControllerNode = currentController.displayNode self.addChild(currentController) + + let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode) + if animated { + currentController.view.layer.animateScale(from: transitionSale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) + currentController.view.layer.allowsGroupOpacity = true + currentController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { completed in + if completed { + currentController.view.layer.allowsGroupOpacity = false + } + commit() + }) + } else { + commit() + } currentController.didMove(toParent: self) currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle - } else { } if let layout = self.validLayout { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index f093c8b3303..ab271c55daf 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -245,7 +245,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1744710921] = { return Api.DocumentAttribute.parse_documentAttributeHasStickers($0) } dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } - dict[389652397] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1137015880] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } dict[761606687] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } @@ -378,6 +378,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } + dict[634962392] = { return Api.InputInvoice.parse_inputInvoiceStarGift($0) } dict[1710230755] = { return Api.InputInvoice.parse_inputInvoiceStars($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } @@ -466,7 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } - dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } + dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) } @@ -500,6 +501,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1560655744] = { return Api.KeyboardButton.parse_keyboardButton($0) } dict[-1344716869] = { return Api.KeyboardButton.parse_keyboardButtonBuy($0) } dict[901503851] = { return Api.KeyboardButton.parse_keyboardButtonCallback($0) } + dict[1976723854] = { return Api.KeyboardButton.parse_keyboardButtonCopy($0) } dict[1358175439] = { return Api.KeyboardButton.parse_keyboardButtonGame($0) } dict[-59151553] = { return Api.KeyboardButton.parse_keyboardButtonRequestGeoLocation($0) } dict[1406648280] = { return Api.KeyboardButton.parse_keyboardButtonRequestPeer($0) } @@ -549,8 +551,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } - dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) } - dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) } + dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) } dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[-2015170219] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) } @@ -572,6 +574,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1434950843] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } + dict[-1682706620] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } @@ -603,7 +606,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1313731771] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } dict[1882335561] = { return Api.MessageMedia.parse_messageMediaContact($0) } dict[1065280907] = { return Api.MessageMedia.parse_messageMediaDice($0) } - dict[1291114285] = { return Api.MessageMedia.parse_messageMediaDocument($0) } + dict[-581497899] = { return Api.MessageMedia.parse_messageMediaDocument($0) } dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) } dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } @@ -628,6 +631,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) } dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } dict[240843065] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) } + dict[2030298073] = { return Api.MessageReportOption.parse_messageReportOption($0) } dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) } dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) } dict[-530392189] = { return Api.MessagesFilter.parse_inputMessagesFilterContacts($0) } @@ -800,6 +804,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[777640226] = { return Api.ReportReason.parse_inputReportReasonPornography($0) } dict[1490799288] = { return Api.ReportReason.parse_inputReportReasonSpam($0) } dict[505595789] = { return Api.ReportReason.parse_inputReportReasonViolence($0) } + dict[1862904881] = { return Api.ReportResult.parse_reportResultAddComment($0) } + dict[-253435722] = { return Api.ReportResult.parse_reportResultChooseOption($0) } + dict[-1917633461] = { return Api.ReportResult.parse_reportResultReported($0) } dict[865857388] = { return Api.RequestPeerType.parse_requestPeerTypeBroadcast($0) } dict[-906990053] = { return Api.RequestPeerType.parse_requestPeerTypeChat($0) } dict[1597737472] = { return Api.RequestPeerType.parse_requestPeerTypeUser($0) } @@ -887,6 +894,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } + dict[1237678029] = { return Api.StarGift.parse_starGift($0) } dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) } dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) } dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) } @@ -894,8 +902,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[-294313259] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[903148150] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } + dict[-110658899] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAPI($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) } @@ -996,7 +1005,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1576161051] = { return Api.Update.parse_updateDeleteMessages($0) } dict[1407644140] = { return Api.Update.parse_updateDeleteQuickReply($0) } dict[1450174413] = { return Api.Update.parse_updateDeleteQuickReplyMessages($0) } - dict[-1870238482] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } + dict[-223929981] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } dict[654302845] = { return Api.Update.parse_updateDialogFilter($0) } dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) } dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) } @@ -1099,9 +1108,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[-2093920310] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } - dict[-862357728] = { return Api.UserFull.parse_userFull($0) } + dict[525919081] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) } + dict[-291202450] = { return Api.UserStarGift.parse_userStarGift($0) } dict[164646985] = { return Api.UserStatus.parse_userStatusEmpty($0) } dict[1703516023] = { return Api.UserStatus.parse_userStatusLastMonth($0) } dict[1410997530] = { return Api.UserStatus.parse_userStatusLastWeek($0) } @@ -1332,16 +1342,20 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1130879648] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) } dict[-512366993] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) } dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) } + dict[-1272590367] = { return Api.payments.PaymentForm.parse_paymentFormStarGift($0) } dict[2079764828] = { return Api.payments.PaymentForm.parse_paymentFormStars($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[-625215430] = { return Api.payments.PaymentReceipt.parse_paymentReceiptStars($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) } + dict[-1877571094] = { return Api.payments.StarGifts.parse_starGifts($0) } + dict[-1551326360] = { return Api.payments.StarGifts.parse_starGiftsNotModified($0) } dict[961445665] = { return Api.payments.StarsRevenueAdsAccountUrl.parse_starsRevenueAdsAccountUrl($0) } dict[-919881925] = { return Api.payments.StarsRevenueStats.parse_starsRevenueStats($0) } dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } dict[-1141231252] = { return Api.payments.StarsStatus.parse_starsStatus($0) } + dict[1801827607] = { return Api.payments.UserStarGifts.parse_userStarGifts($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } dict[-1636664659] = { return Api.phone.GroupCall.parse_groupCall($0) } @@ -1844,6 +1858,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageReplyHeader: _1.serialize(buffer, boxed) + case let _1 as Api.MessageReportOption: + _1.serialize(buffer, boxed) case let _1 as Api.MessageViews: _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: @@ -1960,6 +1976,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ReportReason: _1.serialize(buffer, boxed) + case let _1 as Api.ReportResult: + _1.serialize(buffer, boxed) case let _1 as Api.RequestPeerType: _1.serialize(buffer, boxed) case let _1 as Api.RequestedPeer: @@ -2012,6 +2030,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.SponsoredMessageReportOption: _1.serialize(buffer, boxed) + case let _1 as Api.StarGift: + _1.serialize(buffer, boxed) case let _1 as Api.StarsGiftOption: _1.serialize(buffer, boxed) case let _1 as Api.StarsGiveawayOption: @@ -2092,6 +2112,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.UserProfilePhoto: _1.serialize(buffer, boxed) + case let _1 as Api.UserStarGift: + _1.serialize(buffer, boxed) case let _1 as Api.UserStatus: _1.serialize(buffer, boxed) case let _1 as Api.Username: @@ -2384,6 +2406,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.SavedInfo: _1.serialize(buffer, boxed) + case let _1 as Api.payments.StarGifts: + _1.serialize(buffer, boxed) case let _1 as Api.payments.StarsRevenueAdsAccountUrl: _1.serialize(buffer, boxed) case let _1 as Api.payments.StarsRevenueStats: @@ -2392,6 +2416,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.StarsStatus: _1.serialize(buffer, boxed) + case let _1 as Api.payments.UserStarGifts: + _1.serialize(buffer, boxed) case let _1 as Api.payments.ValidatedRequestedInfo: _1.serialize(buffer, boxed) case let _1 as Api.phone.ExportedGroupCallInvite: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index ed8285c2637..531daed1cba 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -4,6 +4,7 @@ public extension Api { case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) case inputInvoiceSlug(slug: String) + case inputInvoiceStarGift(flags: Int32, userId: Api.InputUser, giftId: Int64, message: Api.TextWithEntities?) case inputInvoiceStars(purpose: Api.InputStorePaymentPurpose) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -34,6 +35,15 @@ public extension Api { } serializeString(slug, buffer: buffer, boxed: false) break + case .inputInvoiceStarGift(let flags, let userId, let giftId, let message): + if boxed { + buffer.appendInt32(634962392) + } + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + serializeInt64(giftId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} + break case .inputInvoiceStars(let purpose): if boxed { buffer.appendInt32(1710230755) @@ -53,6 +63,8 @@ public extension Api { return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) case .inputInvoiceSlug(let slug): return ("inputInvoiceSlug", [("slug", slug as Any)]) + case .inputInvoiceStarGift(let flags, let userId, let giftId, let message): + return ("inputInvoiceStarGift", [("flags", flags as Any), ("userId", userId as Any), ("giftId", giftId as Any), ("message", message as Any)]) case .inputInvoiceStars(let purpose): return ("inputInvoiceStars", [("purpose", purpose as Any)]) } @@ -114,6 +126,30 @@ public extension Api { return nil } } + public static func parse_inputInvoiceStarGift(_ reader: BufferReader) -> InputInvoice? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputUser? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputUser + } + var _3: Int64? + _3 = reader.readInt64() + var _4: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputInvoice.inputInvoiceStarGift(flags: _1!, userId: _2!, giftId: _3!, message: _4) + } + else { + return nil + } + } public static func parse_inputInvoiceStars(_ reader: BufferReader) -> InputInvoice? { var _1: Api.InputStorePaymentPurpose? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index d626af8099f..5a3bc02c8b7 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -711,7 +711,7 @@ public extension Api { public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) - case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64) + case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case inputStorePaymentPremiumSubscription(flags: Int32) case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64) @@ -728,9 +728,9 @@ public extension Api { serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) break - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): if boxed { - buffer.appendInt32(-1551868097) + buffer.appendInt32(-75955309) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -741,6 +741,7 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)} serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): if boxed { @@ -818,8 +819,8 @@ public extension Api { switch self { case .inputStorePaymentGiftPremium(let userId, let currency, let amount): return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): - return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): + return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any), ("message", message as Any)]) case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): @@ -867,13 +868,18 @@ public extension Api { _4 = parseString(reader) var _5: Int64? _5 = reader.readInt64() + var _6: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!) + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 3f76fc2beeb..c5c11feab84 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -674,6 +674,7 @@ public extension Api { case keyboardButton(text: String) case keyboardButtonBuy(text: String) case keyboardButtonCallback(flags: Int32, text: String, data: Buffer) + case keyboardButtonCopy(text: String, copyText: String) case keyboardButtonGame(text: String) case keyboardButtonRequestGeoLocation(text: String) case keyboardButtonRequestPeer(text: String, buttonId: Int32, peerType: Api.RequestPeerType, maxQuantity: Int32) @@ -735,6 +736,13 @@ public extension Api { serializeString(text, buffer: buffer, boxed: false) serializeBytes(data, buffer: buffer, boxed: false) break + case .keyboardButtonCopy(let text, let copyText): + if boxed { + buffer.appendInt32(1976723854) + } + serializeString(text, buffer: buffer, boxed: false) + serializeString(copyText, buffer: buffer, boxed: false) + break case .keyboardButtonGame(let text): if boxed { buffer.appendInt32(1358175439) @@ -838,6 +846,8 @@ public extension Api { return ("keyboardButtonBuy", [("text", text as Any)]) case .keyboardButtonCallback(let flags, let text, let data): return ("keyboardButtonCallback", [("flags", flags as Any), ("text", text as Any), ("data", data as Any)]) + case .keyboardButtonCopy(let text, let copyText): + return ("keyboardButtonCopy", [("text", text as Any), ("copyText", copyText as Any)]) case .keyboardButtonGame(let text): return ("keyboardButtonGame", [("text", text as Any)]) case .keyboardButtonRequestGeoLocation(let text): @@ -968,6 +978,20 @@ public extension Api { return nil } } + public static func parse_keyboardButtonCopy(_ reader: BufferReader) -> KeyboardButton? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.KeyboardButton.keyboardButtonCopy(text: _1!, copyText: _2!) + } + else { + return nil + } + } public static func parse_keyboardButtonGame(_ reader: BufferReader) -> KeyboardButton? { var _1: String? _1 = parseString(reader) diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index b7de916ed3c..ef5c9801fb9 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -986,8 +986,8 @@ public extension Api { case messageActionEmpty case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) - case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) - case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) + case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case messageActionGiveawayLaunch(flags: Int32, stars: Int64?) case messageActionGiveawayResults(flags: Int32, winnersCount: Int32, unclaimedCount: Int32) @@ -1009,6 +1009,7 @@ public extension Api { case messageActionSetChatTheme(emoticon: String) case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) + case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64) case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) @@ -1140,9 +1141,9 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(1737240073) + buffer.appendInt32(1456486804) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)} @@ -1152,10 +1153,11 @@ public extension Api { if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)} break - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(-935499028) + buffer.appendInt32(1818391802) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -1163,6 +1165,7 @@ public extension Api { serializeInt32(months, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): if boxed { @@ -1350,6 +1353,15 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)} break + case .messageActionStarGift(let flags, let gift, let message, let convertStars): + if boxed { + buffer.appendInt32(-1682706620) + } + serializeInt32(flags, buffer: buffer, boxed: false) + gift.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} + serializeInt64(convertStars, buffer: buffer, boxed: false) + break case .messageActionSuggestProfilePhoto(let photo): if boxed { buffer.appendInt32(1474192222) @@ -1429,10 +1441,10 @@ public extension Api { return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)]) case .messageActionGiveawayLaunch(let flags, let stars): @@ -1475,6 +1487,8 @@ public extension Api { return ("messageActionSetChatWallPaper", [("flags", flags as Any), ("wallpaper", wallpaper as Any)]) case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom): return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) + case .messageActionStarGift(let flags, let gift, let message, let convertStars): + return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): @@ -1706,6 +1720,10 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) } var _8: Int64? if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } + var _9: Api.TextWithEntities? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil @@ -1714,8 +1732,9 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8) + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9) } else { return nil @@ -1734,14 +1753,19 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } var _6: Int64? if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } + var _7: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6) + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7) } else { return nil @@ -2106,6 +2130,30 @@ public extension Api { return nil } } + public static func parse_messageActionStarGift(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StarGift? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _3: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4!) + } + else { + return nil + } + } public static func parse_messageActionSuggestProfilePhoto(_ reader: BufferReader) -> MessageAction? { var _1: Api.Photo? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index eedd1393551..77e51989372 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -710,7 +710,7 @@ public extension Api { indirect enum MessageMedia: TypeConstructorDescription { case messageMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String, userId: Int64) case messageMediaDice(value: Int32, emoticon: String) - case messageMediaDocument(flags: Int32, document: Api.Document?, altDocument: Api.Document?, ttlSeconds: Int32?) + case messageMediaDocument(flags: Int32, document: Api.Document?, altDocuments: [Api.Document]?, ttlSeconds: Int32?) case messageMediaEmpty case messageMediaGame(game: Api.Game) case messageMediaGeo(geo: Api.GeoPoint) @@ -745,13 +745,17 @@ public extension Api { serializeInt32(value, buffer: buffer, boxed: false) serializeString(emoticon, buffer: buffer, boxed: false) break - case .messageMediaDocument(let flags, let document, let altDocument, let ttlSeconds): + case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): if boxed { - buffer.appendInt32(1291114285) + buffer.appendInt32(-581497899) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 5) != 0 {altDocument!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(altDocuments!.count)) + for item in altDocuments! { + item.serialize(buffer, true) + }} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(ttlSeconds!, buffer: buffer, boxed: false)} break case .messageMediaEmpty: @@ -905,8 +909,8 @@ public extension Api { return ("messageMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("userId", userId as Any)]) case .messageMediaDice(let value, let emoticon): return ("messageMediaDice", [("value", value as Any), ("emoticon", emoticon as Any)]) - case .messageMediaDocument(let flags, let document, let altDocument, let ttlSeconds): - return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocument", altDocument as Any), ("ttlSeconds", ttlSeconds as Any)]) + case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): + return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocuments", altDocuments as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaEmpty: return ("messageMediaEmpty", []) case .messageMediaGame(let game): @@ -982,9 +986,9 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Document } } - var _3: Api.Document? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Document + var _3: [Api.Document]? + if Int(_1!) & Int(1 << 5) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } } var _4: Int32? if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } @@ -993,7 +997,7 @@ public extension Api { let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocument: _3, ttlSeconds: _4) + return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocuments: _3, ttlSeconds: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 4652a76a82d..38015e3b82e 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -482,6 +482,46 @@ public extension Api { } } +public extension Api { + enum MessageReportOption: TypeConstructorDescription { + case messageReportOption(text: String, option: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReportOption(let text, let option): + if boxed { + buffer.appendInt32(2030298073) + } + serializeString(text, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReportOption(let text, let option): + return ("messageReportOption", [("text", text as Any), ("option", option as Any)]) + } + } + + public static func parse_messageReportOption(_ reader: BufferReader) -> MessageReportOption? { + var _1: String? + _1 = parseString(reader) + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageReportOption.messageReportOption(text: _1!, option: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum MessageViews: TypeConstructorDescription { case messageViews(flags: Int32, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?) @@ -902,87 +942,3 @@ public extension Api { } } -public extension Api { - enum NotificationSound: TypeConstructorDescription { - case notificationSoundDefault - case notificationSoundLocal(title: String, data: String) - case notificationSoundNone - case notificationSoundRingtone(id: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .notificationSoundDefault: - if boxed { - buffer.appendInt32(-1746354498) - } - - break - case .notificationSoundLocal(let title, let data): - if boxed { - buffer.appendInt32(-2096391452) - } - serializeString(title, buffer: buffer, boxed: false) - serializeString(data, buffer: buffer, boxed: false) - break - case .notificationSoundNone: - if boxed { - buffer.appendInt32(1863070943) - } - - break - case .notificationSoundRingtone(let id): - if boxed { - buffer.appendInt32(-9666487) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .notificationSoundDefault: - return ("notificationSoundDefault", []) - case .notificationSoundLocal(let title, let data): - return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)]) - case .notificationSoundNone: - return ("notificationSoundNone", []) - case .notificationSoundRingtone(let id): - return ("notificationSoundRingtone", [("id", id as Any)]) - } - } - - public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? { - return Api.NotificationSound.notificationSoundDefault - } - public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.NotificationSound.notificationSoundLocal(title: _1!, data: _2!) - } - else { - return nil - } - } - public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? { - return Api.NotificationSound.notificationSoundNone - } - public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.NotificationSound.notificationSoundRingtone(id: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index 278c569f5c7..11ea4e2a430 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -1,3 +1,87 @@ +public extension Api { + enum NotificationSound: TypeConstructorDescription { + case notificationSoundDefault + case notificationSoundLocal(title: String, data: String) + case notificationSoundNone + case notificationSoundRingtone(id: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .notificationSoundDefault: + if boxed { + buffer.appendInt32(-1746354498) + } + + break + case .notificationSoundLocal(let title, let data): + if boxed { + buffer.appendInt32(-2096391452) + } + serializeString(title, buffer: buffer, boxed: false) + serializeString(data, buffer: buffer, boxed: false) + break + case .notificationSoundNone: + if boxed { + buffer.appendInt32(1863070943) + } + + break + case .notificationSoundRingtone(let id): + if boxed { + buffer.appendInt32(-9666487) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .notificationSoundDefault: + return ("notificationSoundDefault", []) + case .notificationSoundLocal(let title, let data): + return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)]) + case .notificationSoundNone: + return ("notificationSoundNone", []) + case .notificationSoundRingtone(let id): + return ("notificationSoundRingtone", [("id", id as Any)]) + } + } + + public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? { + return Api.NotificationSound.notificationSoundDefault + } + public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.NotificationSound.notificationSoundLocal(title: _1!, data: _2!) + } + else { + return nil + } + } + public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? { + return Api.NotificationSound.notificationSoundNone + } + public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.NotificationSound.notificationSoundRingtone(id: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum NotifyPeer: TypeConstructorDescription { case notifyBroadcasts diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 73eedd5544f..2e4266d3f59 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -134,6 +134,88 @@ public extension Api { } } +public extension Api { + enum ReportResult: TypeConstructorDescription { + case reportResultAddComment(flags: Int32, option: Buffer) + case reportResultChooseOption(title: String, options: [Api.MessageReportOption]) + case reportResultReported + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reportResultAddComment(let flags, let option): + if boxed { + buffer.appendInt32(1862904881) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + break + case .reportResultChooseOption(let title, let options): + if boxed { + buffer.appendInt32(-253435722) + } + serializeString(title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(options.count)) + for item in options { + item.serialize(buffer, true) + } + break + case .reportResultReported: + if boxed { + buffer.appendInt32(-1917633461) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reportResultAddComment(let flags, let option): + return ("reportResultAddComment", [("flags", flags as Any), ("option", option as Any)]) + case .reportResultChooseOption(let title, let options): + return ("reportResultChooseOption", [("title", title as Any), ("options", options as Any)]) + case .reportResultReported: + return ("reportResultReported", []) + } + } + + public static func parse_reportResultAddComment(_ reader: BufferReader) -> ReportResult? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ReportResult.reportResultAddComment(flags: _1!, option: _2!) + } + else { + return nil + } + } + public static func parse_reportResultChooseOption(_ reader: BufferReader) -> ReportResult? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageReportOption]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageReportOption.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ReportResult.reportResultChooseOption(title: _1!, options: _2!) + } + else { + return nil + } + } + public static func parse_reportResultReported(_ reader: BufferReader) -> ReportResult? { + return Api.ReportResult.reportResultReported + } + + } +} public extension Api { enum RequestPeerType: TypeConstructorDescription { case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?) diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 6372cf2fed0..83c674e9d39 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -572,6 +572,76 @@ public extension Api { } } +public extension Api { + enum StarGift: TypeConstructorDescription { + case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): + if boxed { + buffer.appendInt32(1237678029) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + sticker.serialize(buffer, true) + serializeInt64(stars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityRemains!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)} + serializeInt64(convertStars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(firstSaleDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastSaleDate!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): + return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any)]) + } + } + + public static func parse_starGift(_ reader: BufferReader) -> StarGift? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Api.Document? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Document + } + var _4: Int64? + _4 = reader.readInt64() + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } + var _7: Int64? + _7 = reader.readInt64() + var _8: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_8 = reader.readInt32() } + var _9: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_9 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!, firstSaleDate: _8, lastSaleDate: _9) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsGiftOption: TypeConstructorDescription { case starsGiftOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64) @@ -940,13 +1010,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?, floodskipNumber: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): if boxed { - buffer.appendInt32(-294313259) + buffer.appendInt32(903148150) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -967,14 +1037,16 @@ public extension Api { }} if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipNumber!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any), ("floodskipNumber", floodskipNumber as Any)]) } } @@ -1015,6 +1087,12 @@ public extension Api { if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() } var _15: Int32? if Int(_1!) & Int(1 << 13) != 0 {_15 = reader.readInt32() } + var _16: Api.StarGift? + if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() { + _16 = Api.parse(reader, signature: signature) as? Api.StarGift + } } + var _17: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_17 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1030,8 +1108,10 @@ public extension Api { let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15) + let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipNumber: _17) } else { return nil @@ -1040,113 +1120,3 @@ public extension Api { } } -public extension Api { - enum StarsTransactionPeer: TypeConstructorDescription { - case starsTransactionPeer(peer: Api.Peer) - case starsTransactionPeerAds - case starsTransactionPeerAppStore - case starsTransactionPeerFragment - case starsTransactionPeerPlayMarket - case starsTransactionPeerPremiumBot - case starsTransactionPeerUnsupported - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsTransactionPeer(let peer): - if boxed { - buffer.appendInt32(-670195363) - } - peer.serialize(buffer, true) - break - case .starsTransactionPeerAds: - if boxed { - buffer.appendInt32(1617438738) - } - - break - case .starsTransactionPeerAppStore: - if boxed { - buffer.appendInt32(-1269320843) - } - - break - case .starsTransactionPeerFragment: - if boxed { - buffer.appendInt32(-382740222) - } - - break - case .starsTransactionPeerPlayMarket: - if boxed { - buffer.appendInt32(2069236235) - } - - break - case .starsTransactionPeerPremiumBot: - if boxed { - buffer.appendInt32(621656824) - } - - break - case .starsTransactionPeerUnsupported: - if boxed { - buffer.appendInt32(-1779253276) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsTransactionPeer(let peer): - return ("starsTransactionPeer", [("peer", peer as Any)]) - case .starsTransactionPeerAds: - return ("starsTransactionPeerAds", []) - case .starsTransactionPeerAppStore: - return ("starsTransactionPeerAppStore", []) - case .starsTransactionPeerFragment: - return ("starsTransactionPeerFragment", []) - case .starsTransactionPeerPlayMarket: - return ("starsTransactionPeerPlayMarket", []) - case .starsTransactionPeerPremiumBot: - return ("starsTransactionPeerPremiumBot", []) - case .starsTransactionPeerUnsupported: - return ("starsTransactionPeerUnsupported", []) - } - } - - public static func parse_starsTransactionPeer(_ reader: BufferReader) -> StarsTransactionPeer? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - let _c1 = _1 != nil - if _c1 { - return Api.StarsTransactionPeer.starsTransactionPeer(peer: _1!) - } - else { - return nil - } - } - public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerAds - } - public static func parse_starsTransactionPeerAppStore(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerAppStore - } - public static func parse_starsTransactionPeerFragment(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerFragment - } - public static func parse_starsTransactionPeerPlayMarket(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerPlayMarket - } - public static func parse_starsTransactionPeerPremiumBot(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerPremiumBot - } - public static func parse_starsTransactionPeerUnsupported(_ reader: BufferReader) -> StarsTransactionPeer? { - return Api.StarsTransactionPeer.starsTransactionPeerUnsupported - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index f42e53ce099..950f9bcbf7a 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -1,3 +1,125 @@ +public extension Api { + enum StarsTransactionPeer: TypeConstructorDescription { + case starsTransactionPeer(peer: Api.Peer) + case starsTransactionPeerAPI + case starsTransactionPeerAds + case starsTransactionPeerAppStore + case starsTransactionPeerFragment + case starsTransactionPeerPlayMarket + case starsTransactionPeerPremiumBot + case starsTransactionPeerUnsupported + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsTransactionPeer(let peer): + if boxed { + buffer.appendInt32(-670195363) + } + peer.serialize(buffer, true) + break + case .starsTransactionPeerAPI: + if boxed { + buffer.appendInt32(-110658899) + } + + break + case .starsTransactionPeerAds: + if boxed { + buffer.appendInt32(1617438738) + } + + break + case .starsTransactionPeerAppStore: + if boxed { + buffer.appendInt32(-1269320843) + } + + break + case .starsTransactionPeerFragment: + if boxed { + buffer.appendInt32(-382740222) + } + + break + case .starsTransactionPeerPlayMarket: + if boxed { + buffer.appendInt32(2069236235) + } + + break + case .starsTransactionPeerPremiumBot: + if boxed { + buffer.appendInt32(621656824) + } + + break + case .starsTransactionPeerUnsupported: + if boxed { + buffer.appendInt32(-1779253276) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsTransactionPeer(let peer): + return ("starsTransactionPeer", [("peer", peer as Any)]) + case .starsTransactionPeerAPI: + return ("starsTransactionPeerAPI", []) + case .starsTransactionPeerAds: + return ("starsTransactionPeerAds", []) + case .starsTransactionPeerAppStore: + return ("starsTransactionPeerAppStore", []) + case .starsTransactionPeerFragment: + return ("starsTransactionPeerFragment", []) + case .starsTransactionPeerPlayMarket: + return ("starsTransactionPeerPlayMarket", []) + case .starsTransactionPeerPremiumBot: + return ("starsTransactionPeerPremiumBot", []) + case .starsTransactionPeerUnsupported: + return ("starsTransactionPeerUnsupported", []) + } + } + + public static func parse_starsTransactionPeer(_ reader: BufferReader) -> StarsTransactionPeer? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + let _c1 = _1 != nil + if _c1 { + return Api.StarsTransactionPeer.starsTransactionPeer(peer: _1!) + } + else { + return nil + } + } + public static func parse_starsTransactionPeerAPI(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerAPI + } + public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerAds + } + public static func parse_starsTransactionPeerAppStore(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerAppStore + } + public static func parse_starsTransactionPeerFragment(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerFragment + } + public static func parse_starsTransactionPeerPlayMarket(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerPlayMarket + } + public static func parse_starsTransactionPeerPremiumBot(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerPremiumBot + } + public static func parse_starsTransactionPeerUnsupported(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerUnsupported + } + + } +} public extension Api { enum StatsAbsValueAndPrev: TypeConstructorDescription { case statsAbsValueAndPrev(current: Double, previous: Double) @@ -1056,367 +1178,3 @@ public extension Api { } } -public extension Api { - indirect enum StoryView: TypeConstructorDescription { - case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) - case storyViewPublicForward(flags: Int32, message: Api.Message) - case storyViewPublicRepost(flags: Int32, peerId: Api.Peer, story: Api.StoryItem) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyView(let flags, let userId, let date, let reaction): - if boxed { - buffer.appendInt32(-1329730875) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} - break - case .storyViewPublicForward(let flags, let message): - if boxed { - buffer.appendInt32(-1870436597) - } - serializeInt32(flags, buffer: buffer, boxed: false) - message.serialize(buffer, true) - break - case .storyViewPublicRepost(let flags, let peerId, let story): - if boxed { - buffer.appendInt32(-1116418231) - } - serializeInt32(flags, buffer: buffer, boxed: false) - peerId.serialize(buffer, true) - story.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyView(let flags, let userId, let date, let reaction): - return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) - case .storyViewPublicForward(let flags, let message): - return ("storyViewPublicForward", [("flags", flags as Any), ("message", message as Any)]) - case .storyViewPublicRepost(let flags, let peerId, let story): - return ("storyViewPublicRepost", [("flags", flags as Any), ("peerId", peerId as Any), ("story", story as Any)]) - } - } - - public static func parse_storyView(_ reader: BufferReader) -> StoryView? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Api.Reaction? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Reaction - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) - } - else { - return nil - } - } - public static func parse_storyViewPublicForward(_ reader: BufferReader) -> StoryView? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Message? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Message - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StoryView.storyViewPublicForward(flags: _1!, message: _2!) - } - else { - return nil - } - } - public static func parse_storyViewPublicRepost(_ reader: BufferReader) -> StoryView? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Peer? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _3: Api.StoryItem? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StoryItem - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.StoryView.storyViewPublicRepost(flags: _1!, peerId: _2!, story: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum StoryViews: TypeConstructorDescription { - case storyViews(flags: Int32, viewsCount: Int32, forwardsCount: Int32?, reactions: [Api.ReactionCount]?, reactionsCount: Int32?, recentViewers: [Int64]?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): - if boxed { - buffer.appendInt32(-1923523370) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(viewsCount, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {serializeInt32(forwardsCount!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions!.count)) - for item in reactions! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(reactionsCount!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(recentViewers!.count)) - for item in recentViewers! { - serializeInt64(item, buffer: buffer, boxed: false) - }} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): - return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactions", reactions as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) - } - } - - public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } - var _4: [Api.ReactionCount]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) - } } - var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } - var _6: [Int64]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6) - } - else { - return nil - } - } - - } -} -public extension Api { - enum TextWithEntities: TypeConstructorDescription { - case textWithEntities(text: String, entities: [Api.MessageEntity]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .textWithEntities(let text, let entities): - if boxed { - buffer.appendInt32(1964978502) - } - serializeString(text, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities.count)) - for item in entities { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .textWithEntities(let text, let entities): - return ("textWithEntities", [("text", text as Any), ("entities", entities as Any)]) - } - } - - public static func parse_textWithEntities(_ reader: BufferReader) -> TextWithEntities? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.TextWithEntities.textWithEntities(text: _1!, entities: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum Theme: TypeConstructorDescription { - case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: [Api.ThemeSettings]?, emoticon: String?, installsCount: Int32?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): - if boxed { - buffer.appendInt32(-1609668650) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeString(slug, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(settings!.count)) - for item in settings! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 6) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(installsCount!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): - return ("theme", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("title", title as Any), ("document", document as Any), ("settings", settings as Any), ("emoticon", emoticon as Any), ("installsCount", installsCount as Any)]) - } - } - - public static func parse_theme(_ reader: BufferReader) -> Theme? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Api.Document? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.Document - } } - var _7: [Api.ThemeSettings]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ThemeSettings.self) - } } - var _8: String? - if Int(_1!) & Int(1 << 6) != 0 {_8 = parseString(reader) } - var _9: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, emoticon: _8, installsCount: _9) - } - else { - return nil - } - } - - } -} -public extension Api { - enum ThemeSettings: TypeConstructorDescription { - case themeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.WallPaper?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): - if boxed { - buffer.appendInt32(-94849324) - } - serializeInt32(flags, buffer: buffer, boxed: false) - baseTheme.serialize(buffer, true) - serializeInt32(accentColor, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messageColors!.count)) - for item in messageColors! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): - return ("themeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any)]) - } - } - - public static func parse_themeSettings(_ reader: BufferReader) -> ThemeSettings? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.BaseTheme? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } - var _5: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - var _6: Api.WallPaper? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.WallPaper - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.ThemeSettings.themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 9e553153137..2c19d44fd97 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,367 @@ +public extension Api { + indirect enum StoryView: TypeConstructorDescription { + case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) + case storyViewPublicForward(flags: Int32, message: Api.Message) + case storyViewPublicRepost(flags: Int32, peerId: Api.Peer, story: Api.StoryItem) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyView(let flags, let userId, let date, let reaction): + if boxed { + buffer.appendInt32(-1329730875) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} + break + case .storyViewPublicForward(let flags, let message): + if boxed { + buffer.appendInt32(-1870436597) + } + serializeInt32(flags, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + case .storyViewPublicRepost(let flags, let peerId, let story): + if boxed { + buffer.appendInt32(-1116418231) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peerId.serialize(buffer, true) + story.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyView(let flags, let userId, let date, let reaction): + return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) + case .storyViewPublicForward(let flags, let message): + return ("storyViewPublicForward", [("flags", flags as Any), ("message", message as Any)]) + case .storyViewPublicRepost(let flags, let peerId, let story): + return ("storyViewPublicRepost", [("flags", flags as Any), ("peerId", peerId as Any), ("story", story as Any)]) + } + } + + public static func parse_storyView(_ reader: BufferReader) -> StoryView? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Reaction? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Reaction + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) + } + else { + return nil + } + } + public static func parse_storyViewPublicForward(_ reader: BufferReader) -> StoryView? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Message? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Message + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StoryView.storyViewPublicForward(flags: _1!, message: _2!) + } + else { + return nil + } + } + public static func parse_storyViewPublicRepost(_ reader: BufferReader) -> StoryView? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: Api.StoryItem? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoryItem + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.StoryView.storyViewPublicRepost(flags: _1!, peerId: _2!, story: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StoryViews: TypeConstructorDescription { + case storyViews(flags: Int32, viewsCount: Int32, forwardsCount: Int32?, reactions: [Api.ReactionCount]?, reactionsCount: Int32?, recentViewers: [Int64]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): + if boxed { + buffer.appendInt32(-1923523370) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(viewsCount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(forwardsCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions!.count)) + for item in reactions! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(reactionsCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(recentViewers!.count)) + for item in recentViewers! { + serializeInt64(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): + return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactions", reactions as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) + } + } + + public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } + var _4: [Api.ReactionCount]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) + } } + var _5: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + var _6: [Int64]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TextWithEntities: TypeConstructorDescription { + case textWithEntities(text: String, entities: [Api.MessageEntity]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .textWithEntities(let text, let entities): + if boxed { + buffer.appendInt32(1964978502) + } + serializeString(text, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities.count)) + for item in entities { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .textWithEntities(let text, let entities): + return ("textWithEntities", [("text", text as Any), ("entities", entities as Any)]) + } + } + + public static func parse_textWithEntities(_ reader: BufferReader) -> TextWithEntities? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TextWithEntities.textWithEntities(text: _1!, entities: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum Theme: TypeConstructorDescription { + case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: [Api.ThemeSettings]?, emoticon: String?, installsCount: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): + if boxed { + buffer.appendInt32(-1609668650) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeString(slug, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(settings!.count)) + for item in settings! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 6) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(installsCount!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): + return ("theme", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("title", title as Any), ("document", document as Any), ("settings", settings as Any), ("emoticon", emoticon as Any), ("installsCount", installsCount as Any)]) + } + } + + public static func parse_theme(_ reader: BufferReader) -> Theme? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.Document? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.Document + } } + var _7: [Api.ThemeSettings]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ThemeSettings.self) + } } + var _8: String? + if Int(_1!) & Int(1 << 6) != 0 {_8 = parseString(reader) } + var _9: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, emoticon: _8, installsCount: _9) + } + else { + return nil + } + } + + } +} +public extension Api { + enum ThemeSettings: TypeConstructorDescription { + case themeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.WallPaper?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): + if boxed { + buffer.appendInt32(-94849324) + } + serializeInt32(flags, buffer: buffer, boxed: false) + baseTheme.serialize(buffer, true) + serializeInt32(accentColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messageColors!.count)) + for item in messageColors! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): + return ("themeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any)]) + } + } + + public static func parse_themeSettings(_ reader: BufferReader) -> ThemeSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BaseTheme? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } + var _5: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + var _6: Api.WallPaper? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WallPaper + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.ThemeSettings.themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6) + } + else { + return nil + } + } + + } +} public extension Api { enum Timezone: TypeConstructorDescription { case timezone(id: String, name: String, utcOffset: Int32) @@ -312,7 +676,7 @@ public extension Api { case updateDeleteMessages(messages: [Int32], pts: Int32, ptsCount: Int32) case updateDeleteQuickReply(shortcutId: Int32) case updateDeleteQuickReplyMessages(shortcutId: Int32, messages: [Int32]) - case updateDeleteScheduledMessages(peer: Api.Peer, messages: [Int32]) + case updateDeleteScheduledMessages(flags: Int32, peer: Api.Peer, messages: [Int32], sentMessages: [Int32]?) case updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) case updateDialogFilterOrder(order: [Int32]) case updateDialogFilters @@ -882,16 +1246,22 @@ public extension Api { serializeInt32(item, buffer: buffer, boxed: false) } break - case .updateDeleteScheduledMessages(let peer, let messages): + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): if boxed { - buffer.appendInt32(-1870238482) + buffer.appendInt32(-223929981) } + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { serializeInt32(item, buffer: buffer, boxed: false) } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sentMessages!.count)) + for item in sentMessages! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break case .updateDialogFilter(let flags, let id, let filter): if boxed { @@ -1733,8 +2103,8 @@ public extension Api { return ("updateDeleteQuickReply", [("shortcutId", shortcutId as Any)]) case .updateDeleteQuickReplyMessages(let shortcutId, let messages): return ("updateDeleteQuickReplyMessages", [("shortcutId", shortcutId as Any), ("messages", messages as Any)]) - case .updateDeleteScheduledMessages(let peer, let messages): - return ("updateDeleteScheduledMessages", [("peer", peer as Any), ("messages", messages as Any)]) + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): + return ("updateDeleteScheduledMessages", [("flags", flags as Any), ("peer", peer as Any), ("messages", messages as Any), ("sentMessages", sentMessages as Any)]) case .updateDialogFilter(let flags, let id, let filter): return ("updateDialogFilter", [("flags", flags as Any), ("id", id as Any), ("filter", filter as Any)]) case .updateDialogFilterOrder(let order): @@ -2945,18 +3315,26 @@ public extension Api { } } public static func parse_updateDeleteScheduledMessages(_ reader: BufferReader) -> Update? { - var _1: Api.Peer? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer + _2 = Api.parse(reader, signature: signature) as? Api.Peer } - var _2: [Int32]? + var _3: [Int32]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Update.updateDeleteScheduledMessages(peer: _1!, messages: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.Update.updateDeleteScheduledMessages(flags: _1!, peer: _2!, messages: _3!, sentMessages: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 07aeb17b3be..820f50b8f8c 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -606,13 +606,13 @@ public extension Api { } public extension Api { enum UserFull: TypeConstructorDescription { - case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?) + case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?, stargiftsCount: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage): + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount): if boxed { - buffer.appendInt32(-862357728) + buffer.appendInt32(525919081) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -647,14 +647,15 @@ public extension Api { if Int(flags2) & Int(1 << 5) != 0 {birthday!.serialize(buffer, true)} if Int(flags2) & Int(1 << 6) != 0 {serializeInt64(personalChannelId!, buffer: buffer, boxed: false)} if Int(flags2) & Int(1 << 6) != 0 {serializeInt32(personalChannelMessage!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 8) != 0 {serializeInt32(stargiftsCount!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage): - return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any)]) + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount): + return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any), ("stargiftsCount", stargiftsCount as Any)]) } } @@ -751,6 +752,8 @@ public extension Api { if Int(_2!) & Int(1 << 6) != 0 {_28 = reader.readInt64() } var _29: Int32? if Int(_2!) & Int(1 << 6) != 0 {_29 = reader.readInt32() } + var _30: Int32? + if Int(_2!) & Int(1 << 8) != 0 {_30 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -780,8 +783,9 @@ public extension Api { let _c27 = (Int(_2!) & Int(1 << 5) == 0) || _27 != nil let _c28 = (Int(_2!) & Int(1 << 6) == 0) || _28 != nil let _c29 = (Int(_2!) & Int(1 << 6) == 0) || _29 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 { - return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29) + let _c30 = (Int(_2!) & Int(1 << 8) == 0) || _30 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 { + return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29, stargiftsCount: _30) } else { return nil @@ -850,6 +854,70 @@ public extension Api { } } +public extension Api { + enum UserStarGift: TypeConstructorDescription { + case userStarGift(flags: Int32, fromId: Int64?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, convertStars: Int64?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .userStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let convertStars): + if boxed { + buffer.appendInt32(-291202450) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt64(fromId!, buffer: buffer, boxed: false)} + serializeInt32(date, buffer: buffer, boxed: false) + gift.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {message!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt64(convertStars!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .userStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let convertStars): + return ("userStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("convertStars", convertStars as Any)]) + } + } + + public static func parse_userStarGift(_ reader: BufferReader) -> UserStarGift? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt64() } + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.StarGift? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _5: Api.TextWithEntities? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } + var _6: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_6 = reader.readInt32() } + var _7: Int64? + if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt64() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.UserStarGift.userStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, convertStars: _7) + } + else { + return nil + } + } + + } +} public extension Api { enum UserStatus: TypeConstructorDescription { case userStatusEmpty @@ -1458,207 +1526,3 @@ public extension Api { } } -public extension Api { - enum WebPage: TypeConstructorDescription { - case webPage(flags: Int32, id: Int64, url: String, displayUrl: String, hash: Int32, type: String?, siteName: String?, title: String?, description: String?, photo: Api.Photo?, embedUrl: String?, embedType: String?, embedWidth: Int32?, embedHeight: Int32?, duration: Int32?, author: String?, document: Api.Document?, cachedPage: Api.Page?, attributes: [Api.WebPageAttribute]?) - case webPageEmpty(flags: Int32, id: Int64, url: String?) - case webPageNotModified(flags: Int32, cachedPageViews: Int32?) - case webPagePending(flags: Int32, id: Int64, url: String?, date: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): - if boxed { - buffer.appendInt32(-392411726) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - serializeString(displayUrl, buffer: buffer, boxed: false) - serializeInt32(hash, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(type!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(siteName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeString(description!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {photo!.serialize(buffer, true)} - if Int(flags) & Int(1 << 5) != 0 {serializeString(embedUrl!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 5) != 0 {serializeString(embedType!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 6) != 0 {serializeInt32(embedWidth!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 6) != 0 {serializeInt32(embedHeight!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 7) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 8) != 0 {serializeString(author!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 9) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 10) != 0 {cachedPage!.serialize(buffer, true)} - if Int(flags) & Int(1 << 12) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes!.count)) - for item in attributes! { - item.serialize(buffer, true) - }} - break - case .webPageEmpty(let flags, let id, let url): - if boxed { - buffer.appendInt32(555358088) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} - break - case .webPageNotModified(let flags, let cachedPageViews): - if boxed { - buffer.appendInt32(1930545681) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(cachedPageViews!, buffer: buffer, boxed: false)} - break - case .webPagePending(let flags, let id, let url, let date): - if boxed { - buffer.appendInt32(-1328464313) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} - serializeInt32(date, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): - return ("webPage", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("displayUrl", displayUrl as Any), ("hash", hash as Any), ("type", type as Any), ("siteName", siteName as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("embedUrl", embedUrl as Any), ("embedType", embedType as Any), ("embedWidth", embedWidth as Any), ("embedHeight", embedHeight as Any), ("duration", duration as Any), ("author", author as Any), ("document", document as Any), ("cachedPage", cachedPage as Any), ("attributes", attributes as Any)]) - case .webPageEmpty(let flags, let id, let url): - return ("webPageEmpty", [("flags", flags as Any), ("id", id as Any), ("url", url as Any)]) - case .webPageNotModified(let flags, let cachedPageViews): - return ("webPageNotModified", [("flags", flags as Any), ("cachedPageViews", cachedPageViews as Any)]) - case .webPagePending(let flags, let id, let url, let date): - return ("webPagePending", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("date", date as Any)]) - } - } - - public static func parse_webPage(_ reader: BufferReader) -> WebPage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } - var _7: String? - if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } - var _8: String? - if Int(_1!) & Int(1 << 2) != 0 {_8 = parseString(reader) } - var _9: String? - if Int(_1!) & Int(1 << 3) != 0 {_9 = parseString(reader) } - var _10: Api.Photo? - if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.Photo - } } - var _11: String? - if Int(_1!) & Int(1 << 5) != 0 {_11 = parseString(reader) } - var _12: String? - if Int(_1!) & Int(1 << 5) != 0 {_12 = parseString(reader) } - var _13: Int32? - if Int(_1!) & Int(1 << 6) != 0 {_13 = reader.readInt32() } - var _14: Int32? - if Int(_1!) & Int(1 << 6) != 0 {_14 = reader.readInt32() } - var _15: Int32? - if Int(_1!) & Int(1 << 7) != 0 {_15 = reader.readInt32() } - var _16: String? - if Int(_1!) & Int(1 << 8) != 0 {_16 = parseString(reader) } - var _17: Api.Document? - if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _17 = Api.parse(reader, signature: signature) as? Api.Document - } } - var _18: Api.Page? - if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { - _18 = Api.parse(reader, signature: signature) as? Api.Page - } } - var _19: [Api.WebPageAttribute]? - if Int(_1!) & Int(1 << 12) != 0 {if let _ = reader.readInt32() { - _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebPageAttribute.self) - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 5) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 5) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 6) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 7) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 12) == 0) || _19 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { - return Api.WebPage.webPage(flags: _1!, id: _2!, url: _3!, displayUrl: _4!, hash: _5!, type: _6, siteName: _7, title: _8, description: _9, photo: _10, embedUrl: _11, embedType: _12, embedWidth: _13, embedHeight: _14, duration: _15, author: _16, document: _17, cachedPage: _18, attributes: _19) - } - else { - return nil - } - } - public static func parse_webPageEmpty(_ reader: BufferReader) -> WebPage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.WebPage.webPageEmpty(flags: _1!, id: _2!, url: _3) - } - else { - return nil - } - } - public static func parse_webPageNotModified(_ reader: BufferReader) -> WebPage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - if _c1 && _c2 { - return Api.WebPage.webPageNotModified(flags: _1!, cachedPageViews: _2) - } - else { - return nil - } - } - public static func parse_webPagePending(_ reader: BufferReader) -> WebPage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.WebPage.webPagePending(flags: _1!, id: _2!, url: _3, date: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 2518361353e..8034ebaa398 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,207 @@ +public extension Api { + enum WebPage: TypeConstructorDescription { + case webPage(flags: Int32, id: Int64, url: String, displayUrl: String, hash: Int32, type: String?, siteName: String?, title: String?, description: String?, photo: Api.Photo?, embedUrl: String?, embedType: String?, embedWidth: Int32?, embedHeight: Int32?, duration: Int32?, author: String?, document: Api.Document?, cachedPage: Api.Page?, attributes: [Api.WebPageAttribute]?) + case webPageEmpty(flags: Int32, id: Int64, url: String?) + case webPageNotModified(flags: Int32, cachedPageViews: Int32?) + case webPagePending(flags: Int32, id: Int64, url: String?, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): + if boxed { + buffer.appendInt32(-392411726) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + serializeString(displayUrl, buffer: buffer, boxed: false) + serializeInt32(hash, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(type!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(siteName!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeString(description!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {serializeString(embedUrl!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeString(embedType!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 6) != 0 {serializeInt32(embedWidth!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 6) != 0 {serializeInt32(embedHeight!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {serializeString(author!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 10) != 0 {cachedPage!.serialize(buffer, true)} + if Int(flags) & Int(1 << 12) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes!.count)) + for item in attributes! { + item.serialize(buffer, true) + }} + break + case .webPageEmpty(let flags, let id, let url): + if boxed { + buffer.appendInt32(555358088) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + break + case .webPageNotModified(let flags, let cachedPageViews): + if boxed { + buffer.appendInt32(1930545681) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(cachedPageViews!, buffer: buffer, boxed: false)} + break + case .webPagePending(let flags, let id, let url, let date): + if boxed { + buffer.appendInt32(-1328464313) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): + return ("webPage", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("displayUrl", displayUrl as Any), ("hash", hash as Any), ("type", type as Any), ("siteName", siteName as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("embedUrl", embedUrl as Any), ("embedType", embedType as Any), ("embedWidth", embedWidth as Any), ("embedHeight", embedHeight as Any), ("duration", duration as Any), ("author", author as Any), ("document", document as Any), ("cachedPage", cachedPage as Any), ("attributes", attributes as Any)]) + case .webPageEmpty(let flags, let id, let url): + return ("webPageEmpty", [("flags", flags as Any), ("id", id as Any), ("url", url as Any)]) + case .webPageNotModified(let flags, let cachedPageViews): + return ("webPageNotModified", [("flags", flags as Any), ("cachedPageViews", cachedPageViews as Any)]) + case .webPagePending(let flags, let id, let url, let date): + return ("webPagePending", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("date", date as Any)]) + } + } + + public static func parse_webPage(_ reader: BufferReader) -> WebPage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + var _8: String? + if Int(_1!) & Int(1 << 2) != 0 {_8 = parseString(reader) } + var _9: String? + if Int(_1!) & Int(1 << 3) != 0 {_9 = parseString(reader) } + var _10: Api.Photo? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _11: String? + if Int(_1!) & Int(1 << 5) != 0 {_11 = parseString(reader) } + var _12: String? + if Int(_1!) & Int(1 << 5) != 0 {_12 = parseString(reader) } + var _13: Int32? + if Int(_1!) & Int(1 << 6) != 0 {_13 = reader.readInt32() } + var _14: Int32? + if Int(_1!) & Int(1 << 6) != 0 {_14 = reader.readInt32() } + var _15: Int32? + if Int(_1!) & Int(1 << 7) != 0 {_15 = reader.readInt32() } + var _16: String? + if Int(_1!) & Int(1 << 8) != 0 {_16 = parseString(reader) } + var _17: Api.Document? + if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _17 = Api.parse(reader, signature: signature) as? Api.Document + } } + var _18: Api.Page? + if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { + _18 = Api.parse(reader, signature: signature) as? Api.Page + } } + var _19: [Api.WebPageAttribute]? + if Int(_1!) & Int(1 << 12) != 0 {if let _ = reader.readInt32() { + _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebPageAttribute.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 5) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 5) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 6) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil + let _c15 = (Int(_1!) & Int(1 << 7) == 0) || _15 != nil + let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil + let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 12) == 0) || _19 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { + return Api.WebPage.webPage(flags: _1!, id: _2!, url: _3!, displayUrl: _4!, hash: _5!, type: _6, siteName: _7, title: _8, description: _9, photo: _10, embedUrl: _11, embedType: _12, embedWidth: _13, embedHeight: _14, duration: _15, author: _16, document: _17, cachedPage: _18, attributes: _19) + } + else { + return nil + } + } + public static func parse_webPageEmpty(_ reader: BufferReader) -> WebPage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.WebPage.webPageEmpty(flags: _1!, id: _2!, url: _3) + } + else { + return nil + } + } + public static func parse_webPageNotModified(_ reader: BufferReader) -> WebPage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.WebPage.webPageNotModified(flags: _1!, cachedPageViews: _2) + } + else { + return nil + } + } + public static func parse_webPagePending(_ reader: BufferReader) -> WebPage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.WebPage.webPagePending(flags: _1!, id: _2!, url: _3, date: _4!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum WebPageAttribute: TypeConstructorDescription { case webPageAttributeStickerSet(flags: Int32, stickers: [Api.Document]) @@ -1230,137 +1434,3 @@ public extension Api.account { } } -public extension Api.account { - enum SentEmailCode: TypeConstructorDescription { - case sentEmailCode(emailPattern: String, length: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sentEmailCode(let emailPattern, let length): - if boxed { - buffer.appendInt32(-2128640689) - } - serializeString(emailPattern, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .sentEmailCode(let emailPattern, let length): - return ("sentEmailCode", [("emailPattern", emailPattern as Any), ("length", length as Any)]) - } - } - - public static func parse_sentEmailCode(_ reader: BufferReader) -> SentEmailCode? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.account.SentEmailCode.sentEmailCode(emailPattern: _1!, length: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.account { - enum Takeout: TypeConstructorDescription { - case takeout(id: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .takeout(let id): - if boxed { - buffer.appendInt32(1304052993) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .takeout(let id): - return ("takeout", [("id", id as Any)]) - } - } - - public static func parse_takeout(_ reader: BufferReader) -> Takeout? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.account.Takeout.takeout(id: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.account { - enum Themes: TypeConstructorDescription { - case themes(hash: Int64, themes: [Api.Theme]) - case themesNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .themes(let hash, let themes): - if boxed { - buffer.appendInt32(-1707242387) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(themes.count)) - for item in themes { - item.serialize(buffer, true) - } - break - case .themesNotModified: - if boxed { - buffer.appendInt32(-199313886) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .themes(let hash, let themes): - return ("themes", [("hash", hash as Any), ("themes", themes as Any)]) - case .themesNotModified: - return ("themesNotModified", []) - } - } - - public static func parse_themes(_ reader: BufferReader) -> Themes? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Theme]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Theme.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.account.Themes.themes(hash: _1!, themes: _2!) - } - else { - return nil - } - } - public static func parse_themesNotModified(_ reader: BufferReader) -> Themes? { - return Api.account.Themes.themesNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 7f9b44e542b..54e4821b552 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,137 @@ +public extension Api.account { + enum SentEmailCode: TypeConstructorDescription { + case sentEmailCode(emailPattern: String, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sentEmailCode(let emailPattern, let length): + if boxed { + buffer.appendInt32(-2128640689) + } + serializeString(emailPattern, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .sentEmailCode(let emailPattern, let length): + return ("sentEmailCode", [("emailPattern", emailPattern as Any), ("length", length as Any)]) + } + } + + public static func parse_sentEmailCode(_ reader: BufferReader) -> SentEmailCode? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.SentEmailCode.sentEmailCode(emailPattern: _1!, length: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.account { + enum Takeout: TypeConstructorDescription { + case takeout(id: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .takeout(let id): + if boxed { + buffer.appendInt32(1304052993) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .takeout(let id): + return ("takeout", [("id", id as Any)]) + } + } + + public static func parse_takeout(_ reader: BufferReader) -> Takeout? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.account.Takeout.takeout(id: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.account { + enum Themes: TypeConstructorDescription { + case themes(hash: Int64, themes: [Api.Theme]) + case themesNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .themes(let hash, let themes): + if boxed { + buffer.appendInt32(-1707242387) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(themes.count)) + for item in themes { + item.serialize(buffer, true) + } + break + case .themesNotModified: + if boxed { + buffer.appendInt32(-199313886) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .themes(let hash, let themes): + return ("themes", [("hash", hash as Any), ("themes", themes as Any)]) + case .themesNotModified: + return ("themesNotModified", []) + } + } + + public static func parse_themes(_ reader: BufferReader) -> Themes? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Theme]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Theme.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.Themes.themes(hash: _1!, themes: _2!) + } + else { + return nil + } + } + public static func parse_themesNotModified(_ reader: BufferReader) -> Themes? { + return Api.account.Themes.themesNotModified + } + + } +} public extension Api.account { enum TmpPassword: TypeConstructorDescription { case tmpPassword(tmpPassword: Buffer, validUntil: Int32) @@ -876,97 +1010,3 @@ public extension Api.auth { } } -public extension Api.bots { - enum BotInfo: TypeConstructorDescription { - case botInfo(name: String, about: String, description: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botInfo(let name, let about, let description): - if boxed { - buffer.appendInt32(-391678544) - } - serializeString(name, buffer: buffer, boxed: false) - serializeString(about, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botInfo(let name, let about, let description): - return ("botInfo", [("name", name as Any), ("about", about as Any), ("description", description as Any)]) - } - } - - public static func parse_botInfo(_ reader: BufferReader) -> BotInfo? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.bots.BotInfo.botInfo(name: _1!, about: _2!, description: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.bots { - enum PopularAppBots: TypeConstructorDescription { - case popularAppBots(flags: Int32, nextOffset: String?, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .popularAppBots(let flags, let nextOffset, let users): - if boxed { - buffer.appendInt32(428978491) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .popularAppBots(let flags, let nextOffset, let users): - return ("popularAppBots", [("flags", flags as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) - } - } - - public static func parse_popularAppBots(_ reader: BufferReader) -> PopularAppBots? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.bots.PopularAppBots.popularAppBots(flags: _1!, nextOffset: _2, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 36dea196a1a..ea00fe9e2c1 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,97 @@ +public extension Api.bots { + enum BotInfo: TypeConstructorDescription { + case botInfo(name: String, about: String, description: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botInfo(let name, let about, let description): + if boxed { + buffer.appendInt32(-391678544) + } + serializeString(name, buffer: buffer, boxed: false) + serializeString(about, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botInfo(let name, let about, let description): + return ("botInfo", [("name", name as Any), ("about", about as Any), ("description", description as Any)]) + } + } + + public static func parse_botInfo(_ reader: BufferReader) -> BotInfo? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.bots.BotInfo.botInfo(name: _1!, about: _2!, description: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.bots { + enum PopularAppBots: TypeConstructorDescription { + case popularAppBots(flags: Int32, nextOffset: String?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .popularAppBots(let flags, let nextOffset, let users): + if boxed { + buffer.appendInt32(428978491) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .popularAppBots(let flags, let nextOffset, let users): + return ("popularAppBots", [("flags", flags as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) + } + } + + public static func parse_popularAppBots(_ reader: BufferReader) -> PopularAppBots? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.bots.PopularAppBots.popularAppBots(flags: _1!, nextOffset: _2, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.bots { enum PreviewInfo: TypeConstructorDescription { case previewInfo(media: [Api.BotPreviewMedia], langCodes: [String]) @@ -1398,61 +1492,3 @@ public extension Api.help { } } -public extension Api.help { - enum CountriesList: TypeConstructorDescription { - case countriesList(countries: [Api.help.Country], hash: Int32) - case countriesListNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .countriesList(let countries, let hash): - if boxed { - buffer.appendInt32(-2016381538) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(countries.count)) - for item in countries { - item.serialize(buffer, true) - } - serializeInt32(hash, buffer: buffer, boxed: false) - break - case .countriesListNotModified: - if boxed { - buffer.appendInt32(-1815339214) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .countriesList(let countries, let hash): - return ("countriesList", [("countries", countries as Any), ("hash", hash as Any)]) - case .countriesListNotModified: - return ("countriesListNotModified", []) - } - } - - public static func parse_countriesList(_ reader: BufferReader) -> CountriesList? { - var _1: [Api.help.Country]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.Country.self) - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.help.CountriesList.countriesList(countries: _1!, hash: _2!) - } - else { - return nil - } - } - public static func parse_countriesListNotModified(_ reader: BufferReader) -> CountriesList? { - return Api.help.CountriesList.countriesListNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 8156e1ba080..4cb01769afc 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,61 @@ +public extension Api.help { + enum CountriesList: TypeConstructorDescription { + case countriesList(countries: [Api.help.Country], hash: Int32) + case countriesListNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .countriesList(let countries, let hash): + if boxed { + buffer.appendInt32(-2016381538) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(countries.count)) + for item in countries { + item.serialize(buffer, true) + } + serializeInt32(hash, buffer: buffer, boxed: false) + break + case .countriesListNotModified: + if boxed { + buffer.appendInt32(-1815339214) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .countriesList(let countries, let hash): + return ("countriesList", [("countries", countries as Any), ("hash", hash as Any)]) + case .countriesListNotModified: + return ("countriesListNotModified", []) + } + } + + public static func parse_countriesList(_ reader: BufferReader) -> CountriesList? { + var _1: [Api.help.Country]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.Country.self) + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.help.CountriesList.countriesList(countries: _1!, hash: _2!) + } + else { + return nil + } + } + public static func parse_countriesListNotModified(_ reader: BufferReader) -> CountriesList? { + return Api.help.CountriesList.countriesListNotModified + } + + } +} public extension Api.help { enum Country: TypeConstructorDescription { case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode]) @@ -1236,117 +1294,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum ArchivedStickers: TypeConstructorDescription { - case archivedStickers(count: Int32, sets: [Api.StickerSetCovered]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .archivedStickers(let count, let sets): - if boxed { - buffer.appendInt32(1338747336) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .archivedStickers(let count, let sets): - return ("archivedStickers", [("count", count as Any), ("sets", sets as Any)]) - } - } - - public static func parse_archivedStickers(_ reader: BufferReader) -> ArchivedStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.ArchivedStickers.archivedStickers(count: _1!, sets: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum AvailableEffects: TypeConstructorDescription { - case availableEffects(hash: Int32, effects: [Api.AvailableEffect], documents: [Api.Document]) - case availableEffectsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .availableEffects(let hash, let effects, let documents): - if boxed { - buffer.appendInt32(-1109696146) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(effects.count)) - for item in effects { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(documents.count)) - for item in documents { - item.serialize(buffer, true) - } - break - case .availableEffectsNotModified: - if boxed { - buffer.appendInt32(-772957605) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .availableEffects(let hash, let effects, let documents): - return ("availableEffects", [("hash", hash as Any), ("effects", effects as Any), ("documents", documents as Any)]) - case .availableEffectsNotModified: - return ("availableEffectsNotModified", []) - } - } - - public static func parse_availableEffects(_ reader: BufferReader) -> AvailableEffects? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.AvailableEffect]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AvailableEffect.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.AvailableEffects.availableEffects(hash: _1!, effects: _2!, documents: _3!) - } - else { - return nil - } - } - public static func parse_availableEffectsNotModified(_ reader: BufferReader) -> AvailableEffects? { - return Api.messages.AvailableEffects.availableEffectsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 56ce04bdea8..98d07285675 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,117 @@ +public extension Api.messages { + enum ArchivedStickers: TypeConstructorDescription { + case archivedStickers(count: Int32, sets: [Api.StickerSetCovered]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .archivedStickers(let count, let sets): + if boxed { + buffer.appendInt32(1338747336) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .archivedStickers(let count, let sets): + return ("archivedStickers", [("count", count as Any), ("sets", sets as Any)]) + } + } + + public static func parse_archivedStickers(_ reader: BufferReader) -> ArchivedStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.ArchivedStickers.archivedStickers(count: _1!, sets: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum AvailableEffects: TypeConstructorDescription { + case availableEffects(hash: Int32, effects: [Api.AvailableEffect], documents: [Api.Document]) + case availableEffectsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .availableEffects(let hash, let effects, let documents): + if boxed { + buffer.appendInt32(-1109696146) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(effects.count)) + for item in effects { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents.count)) + for item in documents { + item.serialize(buffer, true) + } + break + case .availableEffectsNotModified: + if boxed { + buffer.appendInt32(-772957605) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .availableEffects(let hash, let effects, let documents): + return ("availableEffects", [("hash", hash as Any), ("effects", effects as Any), ("documents", documents as Any)]) + case .availableEffectsNotModified: + return ("availableEffectsNotModified", []) + } + } + + public static func parse_availableEffects(_ reader: BufferReader) -> AvailableEffects? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.AvailableEffect]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AvailableEffect.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.AvailableEffects.availableEffects(hash: _1!, effects: _2!, documents: _3!) + } + else { + return nil + } + } + public static func parse_availableEffectsNotModified(_ reader: BufferReader) -> AvailableEffects? { + return Api.messages.AvailableEffects.availableEffectsNotModified + } + + } +} public extension Api.messages { enum AvailableReactions: TypeConstructorDescription { case availableReactions(hash: Int32, reactions: [Api.AvailableReaction]) @@ -1342,91 +1456,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum HighScores: TypeConstructorDescription { - case highScores(scores: [Api.HighScore], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .highScores(let scores, let users): - if boxed { - buffer.appendInt32(-1707344487) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(scores.count)) - for item in scores { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .highScores(let scores, let users): - return ("highScores", [("scores", scores as Any), ("users", users as Any)]) - } - } - - public static func parse_highScores(_ reader: BufferReader) -> HighScores? { - var _1: [Api.HighScore]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.HighScore.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.HighScores.highScores(scores: _1!, users: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum HistoryImport: TypeConstructorDescription { - case historyImport(id: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .historyImport(let id): - if boxed { - buffer.appendInt32(375566091) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .historyImport(let id): - return ("historyImport", [("id", id as Any)]) - } - } - - public static func parse_historyImport(_ reader: BufferReader) -> HistoryImport? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.messages.HistoryImport.historyImport(id: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index d31e82a113b..ae3204c4a7a 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1,3 +1,91 @@ +public extension Api.messages { + enum HighScores: TypeConstructorDescription { + case highScores(scores: [Api.HighScore], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .highScores(let scores, let users): + if boxed { + buffer.appendInt32(-1707344487) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(scores.count)) + for item in scores { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .highScores(let scores, let users): + return ("highScores", [("scores", scores as Any), ("users", users as Any)]) + } + } + + public static func parse_highScores(_ reader: BufferReader) -> HighScores? { + var _1: [Api.HighScore]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.HighScore.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.HighScores.highScores(scores: _1!, users: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum HistoryImport: TypeConstructorDescription { + case historyImport(id: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .historyImport(let id): + if boxed { + buffer.appendInt32(375566091) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .historyImport(let id): + return ("historyImport", [("id", id as Any)]) + } + } + + public static func parse_historyImport(_ reader: BufferReader) -> HistoryImport? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.messages.HistoryImport.historyImport(id: _1!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum HistoryImportParsed: TypeConstructorDescription { case historyImportParsed(flags: Int32, title: String?) @@ -1452,85 +1540,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum SponsoredMessages: TypeConstructorDescription { - case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) - case sponsoredMessagesEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): - if boxed { - buffer.appendInt32(-907141753) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .sponsoredMessagesEmpty: - if boxed { - buffer.appendInt32(406407439) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): - return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) - case .sponsoredMessagesEmpty: - return ("sponsoredMessagesEmpty", []) - } - } - - public static func parse_sponsoredMessages(_ reader: BufferReader) -> SponsoredMessages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } - var _3: [Api.SponsoredMessage]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!) - } - else { - return nil - } - } - public static func parse_sponsoredMessagesEmpty(_ reader: BufferReader) -> SponsoredMessages? { - return Api.messages.SponsoredMessages.sponsoredMessagesEmpty - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index c1f54eeea8f..289b3d5fb52 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1,3 +1,85 @@ +public extension Api.messages { + enum SponsoredMessages: TypeConstructorDescription { + case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) + case sponsoredMessagesEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-907141753) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .sponsoredMessagesEmpty: + if boxed { + buffer.appendInt32(406407439) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): + return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .sponsoredMessagesEmpty: + return ("sponsoredMessagesEmpty", []) + } + } + + public static func parse_sponsoredMessages(_ reader: BufferReader) -> SponsoredMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } + var _3: [Api.SponsoredMessage]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + public static func parse_sponsoredMessagesEmpty(_ reader: BufferReader) -> SponsoredMessages? { + return Api.messages.SponsoredMessages.sponsoredMessagesEmpty + } + + } +} public extension Api.messages { enum StickerSet: TypeConstructorDescription { case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) @@ -679,6 +761,7 @@ public extension Api.payments { public extension Api.payments { enum PaymentForm: TypeConstructorDescription { case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: [Api.PaymentSavedCredentials]?, users: [Api.User]) + case paymentFormStarGift(formId: Int64, invoice: Api.Invoice) case paymentFormStars(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -715,6 +798,13 @@ public extension Api.payments { item.serialize(buffer, true) } break + case .paymentFormStarGift(let formId, let invoice): + if boxed { + buffer.appendInt32(-1272590367) + } + serializeInt64(formId, buffer: buffer, boxed: false) + invoice.serialize(buffer, true) + break case .paymentFormStars(let flags, let formId, let botId, let title, let description, let photo, let invoice, let users): if boxed { buffer.appendInt32(2079764828) @@ -739,6 +829,8 @@ public extension Api.payments { switch self { case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): return ("paymentForm", [("flags", flags as Any), ("formId", formId as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("providerId", providerId as Any), ("url", url as Any), ("nativeProvider", nativeProvider as Any), ("nativeParams", nativeParams as Any), ("additionalMethods", additionalMethods as Any), ("savedInfo", savedInfo as Any), ("savedCredentials", savedCredentials as Any), ("users", users as Any)]) + case .paymentFormStarGift(let formId, let invoice): + return ("paymentFormStarGift", [("formId", formId as Any), ("invoice", invoice as Any)]) case .paymentFormStars(let flags, let formId, let botId, let title, let description, let photo, let invoice, let users): return ("paymentFormStars", [("flags", flags as Any), ("formId", formId as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("users", users as Any)]) } @@ -811,6 +903,22 @@ public extension Api.payments { return nil } } + public static func parse_paymentFormStarGift(_ reader: BufferReader) -> PaymentForm? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Api.Invoice? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Invoice + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.payments.PaymentForm.paymentFormStarGift(formId: _1!, invoice: _2!) + } + else { + return nil + } + } public static func parse_paymentFormStars(_ reader: BufferReader) -> PaymentForm? { var _1: Int32? _1 = reader.readInt32() @@ -1128,6 +1236,64 @@ public extension Api.payments { } } +public extension Api.payments { + enum StarGifts: TypeConstructorDescription { + case starGifts(hash: Int32, gifts: [Api.StarGift]) + case starGiftsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starGifts(let hash, let gifts): + if boxed { + buffer.appendInt32(-1877571094) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(gifts.count)) + for item in gifts { + item.serialize(buffer, true) + } + break + case .starGiftsNotModified: + if boxed { + buffer.appendInt32(-1551326360) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starGifts(let hash, let gifts): + return ("starGifts", [("hash", hash as Any), ("gifts", gifts as Any)]) + case .starGiftsNotModified: + return ("starGiftsNotModified", []) + } + } + + public static func parse_starGifts(_ reader: BufferReader) -> StarGifts? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StarGift]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGift.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.payments.StarGifts.starGifts(hash: _1!, gifts: _2!) + } + else { + return nil + } + } + public static func parse_starGiftsNotModified(_ reader: BufferReader) -> StarGifts? { + return Api.payments.StarGifts.starGiftsNotModified + } + + } +} public extension Api.payments { enum StarsRevenueAdsAccountUrl: TypeConstructorDescription { case starsRevenueAdsAccountUrl(url: String) @@ -1341,311 +1507,61 @@ public extension Api.payments { } } public extension Api.payments { - enum ValidatedRequestedInfo: TypeConstructorDescription { - case validatedRequestedInfo(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) + enum UserStarGifts: TypeConstructorDescription { + case userStarGifts(flags: Int32, count: Int32, gifts: [Api.UserStarGift], nextOffset: String?, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .validatedRequestedInfo(let flags, let id, let shippingOptions): + case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): if boxed { - buffer.appendInt32(-784000893) + buffer.appendInt32(1801827607) } serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(id!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(shippingOptions!.count)) - for item in shippingOptions! { - item.serialize(buffer, true) - }} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .validatedRequestedInfo(let flags, let id, let shippingOptions): - return ("validatedRequestedInfo", [("flags", flags as Any), ("id", id as Any), ("shippingOptions", shippingOptions as Any)]) - } - } - - public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } - var _3: [Api.ShippingOption]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) - } } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum ExportedGroupCallInvite: TypeConstructorDescription { - case exportedGroupCallInvite(link: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedGroupCallInvite(let link): - if boxed { - buffer.appendInt32(541839704) - } - serializeString(link, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedGroupCallInvite(let link): - return ("exportedGroupCallInvite", [("link", link as Any)]) - } - } - - public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(link: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupCall: TypeConstructorDescription { - case groupCall(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): - if boxed { - buffer.appendInt32(-1636664659) - } - call.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(participants.count)) - for item in participants { - item.serialize(buffer, true) - } - serializeString(participantsNextOffset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): - return ("groupCall", [("call", call as Any), ("participants", participants as Any), ("participantsNextOffset", participantsNextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { - var _1: Api.GroupCall? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.GroupCall - } - var _2: [Api.GroupCallParticipant]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.phone.GroupCall.groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupCallStreamChannels: TypeConstructorDescription { - case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCallStreamChannels(let channels): - if boxed { - buffer.appendInt32(-790330702) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(channels.count)) - for item in channels { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCallStreamChannels(let channels): - return ("groupCallStreamChannels", [("channels", channels as Any)]) - } - } - - public static func parse_groupCallStreamChannels(_ reader: BufferReader) -> GroupCallStreamChannels? { - var _1: [Api.GroupCallStreamChannel]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallStreamChannel.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.phone.GroupCallStreamChannels.groupCallStreamChannels(channels: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupCallStreamRtmpUrl: TypeConstructorDescription { - case groupCallStreamRtmpUrl(url: String, key: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCallStreamRtmpUrl(let url, let key): - if boxed { - buffer.appendInt32(767505458) - } - serializeString(url, buffer: buffer, boxed: false) - serializeString(key, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCallStreamRtmpUrl(let url, let key): - return ("groupCallStreamRtmpUrl", [("url", url as Any), ("key", key as Any)]) - } - } - - public static func parse_groupCallStreamRtmpUrl(_ reader: BufferReader) -> GroupCallStreamRtmpUrl? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.phone.GroupCallStreamRtmpUrl.groupCallStreamRtmpUrl(url: _1!, key: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupParticipants: TypeConstructorDescription { - case groupParticipants(count: Int32, participants: [Api.GroupCallParticipant], nextOffset: String, chats: [Api.Chat], users: [Api.User], version: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupParticipants(let count, let participants, let nextOffset, let chats, let users, let version): - if boxed { - buffer.appendInt32(-193506890) - } serializeInt32(count, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(participants.count)) - for item in participants { - item.serialize(buffer, true) - } - serializeString(nextOffset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { + buffer.appendInt32(Int32(gifts.count)) + for item in gifts { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) for item in users { item.serialize(buffer, true) } - serializeInt32(version, buffer: buffer, boxed: false) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupParticipants(let count, let participants, let nextOffset, let chats, let users, let version): - return ("groupParticipants", [("count", count as Any), ("participants", participants as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any), ("version", version as Any)]) + case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): + return ("userStarGifts", [("flags", flags as Any), ("count", count as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) } } - public static func parse_groupParticipants(_ reader: BufferReader) -> GroupParticipants? { + public static func parse_userStarGifts(_ reader: BufferReader) -> UserStarGifts? { var _1: Int32? _1 = reader.readInt32() - var _2: [Api.GroupCallParticipant]? + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.UserStarGift]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.UserStarGift.self) } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } var _5: [Api.User]? if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } - var _6: Int32? - _6 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.phone.GroupParticipants.groupParticipants(count: _1!, participants: _2!, nextOffset: _3!, chats: _4!, users: _5!, version: _6!) + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.UserStarGifts.userStarGifts(flags: _1!, count: _2!, gifts: _3!, nextOffset: _4, users: _5!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 1e10a978c45..222cf9735bf 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,317 @@ +public extension Api.payments { + enum ValidatedRequestedInfo: TypeConstructorDescription { + case validatedRequestedInfo(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .validatedRequestedInfo(let flags, let id, let shippingOptions): + if boxed { + buffer.appendInt32(-784000893) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(id!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(shippingOptions!.count)) + for item in shippingOptions! { + item.serialize(buffer, true) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .validatedRequestedInfo(let flags, let id, let shippingOptions): + return ("validatedRequestedInfo", [("flags", flags as Any), ("id", id as Any), ("shippingOptions", shippingOptions as Any)]) + } + } + + public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: [Api.ShippingOption]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum ExportedGroupCallInvite: TypeConstructorDescription { + case exportedGroupCallInvite(link: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedGroupCallInvite(let link): + if boxed { + buffer.appendInt32(541839704) + } + serializeString(link, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedGroupCallInvite(let link): + return ("exportedGroupCallInvite", [("link", link as Any)]) + } + } + + public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(link: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupCall: TypeConstructorDescription { + case groupCall(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): + if boxed { + buffer.appendInt32(-1636664659) + } + call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(participants.count)) + for item in participants { + item.serialize(buffer, true) + } + serializeString(participantsNextOffset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): + return ("groupCall", [("call", call as Any), ("participants", participants as Any), ("participantsNextOffset", participantsNextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { + var _1: Api.GroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.GroupCall + } + var _2: [Api.GroupCallParticipant]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.phone.GroupCall.groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupCallStreamChannels: TypeConstructorDescription { + case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallStreamChannels(let channels): + if boxed { + buffer.appendInt32(-790330702) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(channels.count)) + for item in channels { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallStreamChannels(let channels): + return ("groupCallStreamChannels", [("channels", channels as Any)]) + } + } + + public static func parse_groupCallStreamChannels(_ reader: BufferReader) -> GroupCallStreamChannels? { + var _1: [Api.GroupCallStreamChannel]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallStreamChannel.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.phone.GroupCallStreamChannels.groupCallStreamChannels(channels: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupCallStreamRtmpUrl: TypeConstructorDescription { + case groupCallStreamRtmpUrl(url: String, key: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallStreamRtmpUrl(let url, let key): + if boxed { + buffer.appendInt32(767505458) + } + serializeString(url, buffer: buffer, boxed: false) + serializeString(key, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallStreamRtmpUrl(let url, let key): + return ("groupCallStreamRtmpUrl", [("url", url as Any), ("key", key as Any)]) + } + } + + public static func parse_groupCallStreamRtmpUrl(_ reader: BufferReader) -> GroupCallStreamRtmpUrl? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.phone.GroupCallStreamRtmpUrl.groupCallStreamRtmpUrl(url: _1!, key: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupParticipants: TypeConstructorDescription { + case groupParticipants(count: Int32, participants: [Api.GroupCallParticipant], nextOffset: String, chats: [Api.Chat], users: [Api.User], version: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupParticipants(let count, let participants, let nextOffset, let chats, let users, let version): + if boxed { + buffer.appendInt32(-193506890) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(participants.count)) + for item in participants { + item.serialize(buffer, true) + } + serializeString(nextOffset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + serializeInt32(version, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupParticipants(let count, let participants, let nextOffset, let chats, let users, let version): + return ("groupParticipants", [("count", count as Any), ("participants", participants as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any), ("version", version as Any)]) + } + } + + public static func parse_groupParticipants(_ reader: BufferReader) -> GroupParticipants? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.GroupCallParticipant]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: Int32? + _6 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.phone.GroupParticipants.groupParticipants(count: _1!, participants: _2!, nextOffset: _3!, chats: _4!, users: _5!, version: _6!) + } + else { + return nil + } + } + + } +} public extension Api.phone { enum JoinAsPeers: TypeConstructorDescription { case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) @@ -1352,187 +1666,3 @@ public extension Api.storage { } } -public extension Api.stories { - enum AllStories: TypeConstructorDescription { - case allStories(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) - case allStoriesNotModified(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): - if boxed { - buffer.appendInt32(1862033025) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - serializeString(state, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peerStories.count)) - for item in peerStories { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - stealthMode.serialize(buffer, true) - break - case .allStoriesNotModified(let flags, let state, let stealthMode): - if boxed { - buffer.appendInt32(291044926) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(state, buffer: buffer, boxed: false) - stealthMode.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): - return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("peerStories", peerStories as Any), ("chats", chats as Any), ("users", users as Any), ("stealthMode", stealthMode as Any)]) - case .allStoriesNotModified(let flags, let state, let stealthMode): - return ("allStoriesNotModified", [("flags", flags as Any), ("state", state as Any), ("stealthMode", stealthMode as Any)]) - } - } - - public static func parse_allStories(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: [Api.PeerStories]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _7: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!) - } - else { - return nil - } - } - public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.AllStories.allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum FoundStories: TypeConstructorDescription { - case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): - if boxed { - buffer.appendInt32(-488736969) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stories.count)) - for item in stories { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): - return ("foundStories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_foundStories(_ reader: BufferReader) -> FoundStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.FoundStory]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundStory.self) - } - var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.FoundStories.foundStories(flags: _1!, count: _2!, stories: _3!, nextOffset: _4, chats: _5!, users: _6!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index 0e9dfa8a858..969678e990a 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,187 @@ +public extension Api.stories { + enum AllStories: TypeConstructorDescription { + case allStories(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) + case allStoriesNotModified(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): + if boxed { + buffer.appendInt32(1862033025) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeString(state, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peerStories.count)) + for item in peerStories { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + stealthMode.serialize(buffer, true) + break + case .allStoriesNotModified(let flags, let state, let stealthMode): + if boxed { + buffer.appendInt32(291044926) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(state, buffer: buffer, boxed: false) + stealthMode.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): + return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("peerStories", peerStories as Any), ("chats", chats as Any), ("users", users as Any), ("stealthMode", stealthMode as Any)]) + case .allStoriesNotModified(let flags, let state, let stealthMode): + return ("allStoriesNotModified", [("flags", flags as Any), ("state", state as Any), ("stealthMode", stealthMode as Any)]) + } + } + + public static func parse_allStories(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: [Api.PeerStories]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _7: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!) + } + else { + return nil + } + } + public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.AllStories.allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum FoundStories: TypeConstructorDescription { + case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): + if boxed { + buffer.appendInt32(-488736969) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stories.count)) + for item in stories { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): + return ("foundStories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_foundStories(_ reader: BufferReader) -> FoundStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.FoundStory]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundStory.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.FoundStories.foundStories(flags: _1!, count: _2!, stories: _3!, nextOffset: _4, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum PeerStories: TypeConstructorDescription { case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 8d34e43419d..0a6196d7bb5 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2607,22 +2607,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func clickSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(414170259) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.clickSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.channels { static func convertToGigagroup(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3164,21 +3148,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func getSponsoredMessages(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-333377601) - channel.serialize(buffer, true) - return (FunctionDescription(name: "channels.getSponsoredMessages", parameters: [("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in - let reader = BufferReader(buffer) - var result: Api.messages.SponsoredMessages? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.messages.SponsoredMessages - } - return result - }) - } -} public extension Api.functions.channels { static func inviteToChannel(channel: Api.InputChannel, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3343,23 +3312,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func reportSponsoredMessage(channel: Api.InputChannel, randomId: Buffer, option: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1349519687) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - serializeBytes(option, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.reportSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId)), ("option", String(describing: option))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.SponsoredMessageReportResult? in - let reader = BufferReader(buffer) - var result: Api.channels.SponsoredMessageReportResult? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.channels.SponsoredMessageReportResult - } - return result - }) - } -} public extension Api.functions.channels { static func restrictSponsoredMessages(channel: Api.InputChannel, restricted: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3687,22 +3639,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func viewSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1095836780) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.viewSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.chatlists { static func checkChatlistInvite(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5006,6 +4942,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func clickSponsoredMessage(flags: Int32, peer: Api.InputPeer, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(252261477) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.clickSponsoredMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func createChat(flags: Int32, users: [Api.InputUser], title: String, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6837,6 +6790,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getSponsoredMessages(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1680673735) + peer.serialize(buffer, true) + return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in + let reader = BufferReader(buffer) + var result: Api.messages.SponsoredMessages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.SponsoredMessages + } + return result + }) + } +} public extension Api.functions.messages { static func getStickerSet(stickerset: Api.InputStickerSet, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7379,22 +7347,22 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1991005362) + buffer.appendInt32(-59199589) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(id.count)) for item in id { serializeInt32(item, buffer: buffer, boxed: false) } - reason.serialize(buffer, true) + serializeBytes(option, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.ReportResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.ReportResult } return result }) @@ -7447,6 +7415,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func reportSponsoredMessage(peer: Api.InputPeer, randomId: Buffer, option: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(452189112) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.reportSponsoredMessage", parameters: [("peer", String(describing: peer)), ("randomId", String(describing: randomId)), ("option", String(describing: option))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.SponsoredMessageReportResult? in + let reader = BufferReader(buffer) + var result: Api.channels.SponsoredMessageReportResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.channels.SponsoredMessageReportResult + } + return result + }) + } +} public extension Api.functions.messages { static func requestAppWebView(flags: Int32, peer: Api.InputPeer, app: Api.InputBotApp, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8727,6 +8712,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func viewSponsoredMessage(peer: Api.InputPeer, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1731909873) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.viewSponsoredMessage", parameters: [("peer", String(describing: peer)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8837,6 +8838,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func convertStarGift(userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(69328935) + userId.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.convertStarGift", parameters: [("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func exportInvoice(invoiceMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8963,6 +8980,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarGifts(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1000983152) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getStarGifts", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarGifts? in + let reader = BufferReader(buffer) + var result: Api.payments.StarGifts? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarGifts + } + return result + }) + } +} public extension Api.functions.payments { static func getStarsGiftOptions(flags: Int32, userId: Api.InputUser?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.StarsGiftOption]>) { let buffer = Buffer() @@ -9128,6 +9160,23 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getUserStarGifts(userId: Api.InputUser, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1584580577) + userId.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getUserStarGifts", parameters: [("userId", String(describing: userId)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.UserStarGifts? in + let reader = BufferReader(buffer) + var result: Api.payments.UserStarGifts? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.UserStarGifts + } + return result + }) + } +} public extension Api.functions.payments { static func launchPrepaidGiveaway(peer: Api.InputPeer, giveawayId: Int64, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -9161,6 +9210,23 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func saveStarGift(flags: Int32, userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2018709362) + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.saveStarGift", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -9183,13 +9249,12 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func sendStarsForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendStarsForm(formId: Int64, invoice: Api.InputInvoice) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(45839133) - serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(2040056084) serializeInt64(formId, buffer: buffer, boxed: false) invoice.serialize(buffer, true) - return (FunctionDescription(name: "payments.sendStarsForm", parameters: [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("invoice", String(describing: invoice))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in + return (FunctionDescription(name: "payments.sendStarsForm", parameters: [("formId", String(describing: formId)), ("invoice", String(describing: invoice))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in let reader = BufferReader(buffer) var result: Api.payments.PaymentResult? if let signature = reader.readInt32() { @@ -10038,12 +10103,12 @@ public extension Api.functions.smsjobs { } } public extension Api.functions.stats { - static func getBroadcastRevenueStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueStats(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1977595505) + buffer.appendInt32(-142021095) serializeInt32(flags, buffer: buffer, boxed: false) - channel.serialize(buffer, true) - return (FunctionDescription(name: "stats.getBroadcastRevenueStats", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueStats? in + peer.serialize(buffer, true) + return (FunctionDescription(name: "stats.getBroadcastRevenueStats", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueStats? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueStats? if let signature = reader.readInt32() { @@ -10054,13 +10119,13 @@ public extension Api.functions.stats { } } public extension Api.functions.stats { - static func getBroadcastRevenueTransactions(channel: Api.InputChannel, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueTransactions(peer: Api.InputPeer, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(6891535) - channel.serialize(buffer, true) + buffer.appendInt32(1889078125) + peer.serialize(buffer, true) serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stats.getBroadcastRevenueTransactions", parameters: [("channel", String(describing: channel)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueTransactions? in + return (FunctionDescription(name: "stats.getBroadcastRevenueTransactions", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueTransactions? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueTransactions? if let signature = reader.readInt32() { @@ -10071,12 +10136,12 @@ public extension Api.functions.stats { } } public extension Api.functions.stats { - static func getBroadcastRevenueWithdrawalUrl(channel: Api.InputChannel, password: Api.InputCheckPasswordSRP) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueWithdrawalUrl(peer: Api.InputPeer, password: Api.InputCheckPasswordSRP) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(711323507) - channel.serialize(buffer, true) + buffer.appendInt32(-1644889427) + peer.serialize(buffer, true) password.serialize(buffer, true) - return (FunctionDescription(name: "stats.getBroadcastRevenueWithdrawalUrl", parameters: [("channel", String(describing: channel)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueWithdrawalUrl? in + return (FunctionDescription(name: "stats.getBroadcastRevenueWithdrawalUrl", parameters: [("peer", String(describing: peer)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueWithdrawalUrl? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueWithdrawalUrl? if let signature = reader.readInt32() { @@ -10722,37 +10787,38 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(421788300) + buffer.appendInt32(433646405) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(id.count)) for item in id { serializeInt32(item, buffer: buffer, boxed: false) } - reason.serialize(buffer, true) + serializeBytes(option, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.ReportResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.ReportResult } return result }) } } public extension Api.functions.stories { - static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, peer: Api.InputPeer?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1827279210) + buffer.appendInt32(-780072697) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {area!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {peer!.serialize(buffer, true)} serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in + return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in let reader = BufferReader(buffer) var result: Api.stories.FoundStories? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index bd121a4a0b6..98c4ef46fc9 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1473,7 +1473,7 @@ public extension Api { case documentAttributeHasStickers case documentAttributeImageSize(w: Int32, h: Int32) case documentAttributeSticker(flags: Int32, alt: String, stickerset: Api.InputStickerSet, maskCoords: Api.MaskCoords?) - case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?, videoStartTs: Double?) + case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?, videoStartTs: Double?, videoCodec: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1529,9 +1529,9 @@ public extension Api { stickerset.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {maskCoords!.serialize(buffer, true)} break - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs, let videoCodec): if boxed { - buffer.appendInt32(389652397) + buffer.appendInt32(1137015880) } serializeInt32(flags, buffer: buffer, boxed: false) serializeDouble(duration, buffer: buffer, boxed: false) @@ -1539,6 +1539,7 @@ public extension Api { serializeInt32(h, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {serializeInt32(preloadPrefixSize!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeString(videoCodec!, buffer: buffer, boxed: false)} break } } @@ -1559,8 +1560,8 @@ public extension Api { return ("documentAttributeImageSize", [("w", w as Any), ("h", h as Any)]) case .documentAttributeSticker(let flags, let alt, let stickerset, let maskCoords): return ("documentAttributeSticker", [("flags", flags as Any), ("alt", alt as Any), ("stickerset", stickerset as Any), ("maskCoords", maskCoords as Any)]) - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): - return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any), ("videoStartTs", videoStartTs as Any)]) + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs, let videoCodec): + return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any), ("videoStartTs", videoStartTs as Any), ("videoCodec", videoCodec as Any)]) } } @@ -1674,14 +1675,17 @@ public extension Api { if Int(_1!) & Int(1 << 2) != 0 {_5 = reader.readInt32() } var _6: Double? if Int(_1!) & Int(1 << 4) != 0 {_6 = reader.readDouble() } + var _7: String? + if Int(_1!) & Int(1 << 5) != 0 {_7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6) + let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6, videoCodec: _7) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 934725f5431..701a5c7ff05 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -18,7 +18,10 @@ import TelegramVoip import MetalEngine import DeviceAccess import LibYuvBinding - +// MARK: Nicegram NCG-5828 call recording +import NGStrings +import UndoUI +// final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol { private struct PanGestureState { var offsetFraction: CGFloat @@ -155,7 +158,30 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.restoreUIForPictureInPicture?(completion) } - +// MARK: Nicegram NCG-5828 call recording + self.callScreen.recordAction = { [weak self] in + guard let self, + let callScreenState else { + return + } + + let isCallRecord = !callScreenState.isCallRecord + if isCallRecord { + self.sharedContext.callManager?.startRecordCall { [weak self] in + self?.sharedContext.callManager?.showRecordSaveToast() + } + self.updateCallRecordButton(with: isCallRecord) + } else { + self.showRecordSaveAlert { [self] in + self.updateCallRecordButton(with: isCallRecord) + } + } + } + + self.sharedContext.callManager?.callCompletion = { [weak self] in + self?.sharedContext.callManager?.showRecordSaveToast() + } +// MARK: Nicegram NCG-5828 call recording, isCallRecord self.callScreenState = PrivateCallScreen.State( strings: presentationData.strings, lifecycleState: .connecting, @@ -168,7 +194,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false, - isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency + isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency, + isCallRecord: false ) if let peer = call.peer { self.updatePeer(peer: peer) @@ -556,6 +583,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) { self.updatePeer(peer: EnginePeer(peer)) +// MARK: Nicegram NCG-5828 call recording + self.sharedContext.callManager?.setupPeer(peer: EnginePeer(peer)) +// } private func updatePeer(peer: EnginePeer) { @@ -720,6 +750,41 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP ) } } +// MARK: Nicegram NCG-5828 call recording + private func showRecordSaveAlert(with completion: @escaping () -> Void) { + let alertController = UIAlertController( + title: l("NicegramCallRecord.StopAlertTitle"), + message: l("NicegramCallRecord.StopAlertDescription"), + preferredStyle: .alert + ) + + alertController.addAction(.init( + title: l("NicegramCallRecord.StopAlertButtonCancel"), + style: .cancel + )) + alertController.addAction(.init( + title: l("NicegramCallRecord.StopAlertButtonStop"), + style: .default, + handler: { _ in + completion() + self.callScreen.stopRecordTimer() + self.sharedContext.callManager?.stopRecordCall() + } + )) + + sharedContext.mainWindow?.presentNative(alertController) + } + + private func updateCallRecordButton(with isCallRecord: Bool) { + guard var callScreenState = self.callScreenState else { + return + } + + callScreenState.isCallRecord = isCallRecord + self.callScreenState = callScreenState + self.update(transition: .animated(duration: 0.3, curve: .spring)) + } +// } private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool { diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index d9c9d9b8921..fa86baa08a7 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -291,7 +291,7 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int let id = Int64.random(in: Int64.min ... Int64.max) let name = "\(callId.id)_\(callId.accessHash).log.json" let path = callLogsPath(account: engine.account) + "/" + name - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) let message = EnqueueMessage.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) return rate |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [message]) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index bd564a7b95e..57469c6c550 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -127,7 +127,9 @@ public final class PresentationCallImpl: PresentationCall { private let screencastFramesDisposable = MetaDisposable() private let screencastAudioDataDisposable = MetaDisposable() private let screencastStateDisposable = MetaDisposable() - +// MARK: Nicegram NCG-5828 call recording + var callActiveState: ((OngoingCallContext.AudioDevice?) -> Void)? +// init( context: AccountContext, audioSession: ManagedAudioSession, @@ -732,6 +734,11 @@ public final class PresentationCallImpl: PresentationCall { self.isAudioSessionActive = value } self.sharedAudioDevice?.setIsAudioSessionActive(value) +// MARK: Nicegram NCG-5828 call recording + if value { + callActiveState?(sharedAudioDevice) + } +// } public func answer() { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 591844aac67..787bd98691e 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -12,7 +12,13 @@ import TelegramUIPreferences import AccountContext import CallKit import PhoneNumberFormat - +// MARK: Nicegram NCG-5828 call recording +import NGLogging +import NGData +import NGStrings +import UndoUI +import NGUtils +// private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? { let enabled = settings?.enableSystemIntegration ?? true return enabled ? integration : nil @@ -317,7 +323,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue - + let call = PresentationCallImpl( context: firstState.0, audioSession: strongSelf.audioSession, @@ -341,6 +347,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager { enableTCP: experimentalSettings.enableVoipTcp, preferredVideoCodec: experimentalSettings.preferredVideoCodec ) +// MARK: Nicegram NCG-5828 call recording, callState + call.callActiveState = { [strongSelf] audioDevice in + strongSelf.callActiveState( + with: audioDevice, + accountContext: firstState.0 + ) + } +// strongSelf.updateCurrentCall(call) strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActivePersonalCallsPromise.set(true) @@ -561,7 +575,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let isVideoPossible: Bool = areVideoCallsAvailable let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings - + let call = PresentationCallImpl( context: context, audioSession: strongSelf.audioSession, @@ -588,6 +602,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager { enableTCP: experimentalSettings.enableVoipTcp, preferredVideoCodec: experimentalSettings.preferredVideoCodec ) +// MARK: Nicegram NCG-5828 call recording, callState + call.callActiveState = { [strongSelf] audioDevice in + strongSelf.callActiveState( + with: audioDevice, + accountContext: context + ) + } +// strongSelf.updateCurrentCall(call) strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActivePersonalCallsPromise.set(true) @@ -639,7 +661,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } - private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { + private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId(), parentController: ViewController) -> Signal { let (presentationData, present, openSettings) = self.getDeviceAccessData() let isVideo = false @@ -673,7 +695,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) ) |> deliverOnMainQueue - |> mapToSignal { [weak self] accessEnabled, peer -> Signal in + |> mapToSignal { [weak self, weak parentController] accessEnabled, peer -> Signal in guard let strongSelf = self else { return .single(false) } @@ -686,46 +708,98 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info { isChannel = true } - - let call = PresentationGroupCallImpl( - accountContext: accountContext, - audioSession: strongSelf.audioSession, - callKitIntegration: nil, - getDeviceAccessData: strongSelf.getDeviceAccessData, - initialCall: nil, - internalId: internalId, - peerId: peerId, - isChannel: isChannel, - invite: nil, - joinAsPeerId: nil, - isStream: false - ) - strongSelf.updateCurrentGroupCall(call) - strongSelf.currentGroupCallPromise.set(.single(call)) - strongSelf.hasActiveGroupCallsPromise.set(true) - strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak call] value in - guard let strongSelf = self, let call = call else { - return + + if shouldUseV2VideoChatImpl(context: accountContext) { + if let parentController { + parentController.push(ScheduleVideoChatSheetScreen( + context: accountContext, + scheduleAction: { timestamp in + guard let self else { + return + } + + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: self.audioSession, + callKitIntegration: nil, + getDeviceAccessData: self.getDeviceAccessData, + initialCall: nil, + internalId: internalId, + peerId: peerId, + isChannel: isChannel, + invite: nil, + joinAsPeerId: nil, + isStream: false + ) + call.schedule(timestamp: timestamp) + + self.updateCurrentGroupCall(call) + self.currentGroupCallPromise.set(.single(call)) + self.hasActiveGroupCallsPromise.set(true) + self.removeCurrentGroupCallDisposable.set((call.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak call] value in + guard let self, let call else { + return + } + if value { + if self.currentGroupCall === call { + self.updateCurrentGroupCall(nil) + self.currentGroupCallPromise.set(.single(nil)) + self.hasActiveGroupCallsPromise.set(false) + } + } + })) + } + )) } - if value { - if strongSelf.currentGroupCall === call { - strongSelf.updateCurrentGroupCall(nil) - strongSelf.currentGroupCallPromise.set(.single(nil)) - strongSelf.hasActiveGroupCallsPromise.set(false) + + return .single(true) + } else { + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: strongSelf.audioSession, + callKitIntegration: nil, + getDeviceAccessData: strongSelf.getDeviceAccessData, + initialCall: nil, + internalId: internalId, + peerId: peerId, + isChannel: isChannel, + invite: nil, + joinAsPeerId: nil, + isStream: false + ) + strongSelf.updateCurrentGroupCall(call) + strongSelf.currentGroupCallPromise.set(.single(call)) + strongSelf.hasActiveGroupCallsPromise.set(true) + strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak call] value in + guard let strongSelf = self, let call = call else { + return } - } - })) + if value { + if strongSelf.currentGroupCall === call { + strongSelf.updateCurrentGroupCall(nil) + strongSelf.currentGroupCallPromise.set(.single(nil)) + strongSelf.hasActiveGroupCallsPromise.set(false) + } + } + })) + } return .single(true) } } - public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult { - let begin: () -> Void = { [weak self] in - let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start() + public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult { + let begin: () -> Void = { [weak self, weak parentController] in + guard let parentController else { + return + } + let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId, parentController: parentController).start() } if let currentGroupCall = self.currentGroupCallValue { @@ -898,4 +972,183 @@ public final class PresentationCallManagerImpl: PresentationCallManager { return .single(true) } } +// MARK: Nicegram NCG-5828 call recording + public var callCompletion: (() -> Void)? + + private weak var audioDevice: OngoingCallContext.AudioDevice? + private var accountContext: AccountContext? + private var enginePeer: EnginePeer? + private var userDisplayName: String { + guard let enginePeer else { return "" } + + switch enginePeer { + case let .user(telegramUser): + if let firstName = telegramUser.firstName, !firstName.isEmpty { + if let lastName = telegramUser.lastName, !lastName.isEmpty { + return "\(firstName) \(lastName)" + } else { + return firstName + } + } else if let username = telegramUser.username, !username.isEmpty { + return username + } else { + return "" + } + default: return "" + } + } + private let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd-MM-yyyy" + + return dateFormatter + }() + + private func deleteFile(from path: String) { + let fileManager = FileManager.default + + if fileManager.fileExists(atPath: path) { + do { + let fileURL = URL(fileURLWithPath: path) + try fileManager.removeItem(at: fileURL) + } catch { + ngLog("[Call Recorder] Error remove call file: \(error.localizedDescription)") + } + } else { + ngLog("[Call Recorder] Call file not found at path \(path)") + } + } + + private func writeAudioToSaved( + from path: String, + duration: Double, + size: UInt, + completion: (() -> Void)? = nil + ) { + let date = Date() + + let id = Int64.random(in: 0 ... Int64.max) + let resource = LocalFileReferenceMediaResource( + localFilePath: path, + randomId: id + ) + + let file = TelegramMediaFile( + fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: id), + partialReference: nil, + resource: resource, + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, +// mimeType: "audio/ogg", + mimeType: "audio/wav", + size: Int64(size), + attributes: [ + .Audio( + isVoice: false, + duration: Int(duration), + title: "\(userDisplayName)-\(dateFormatter.string(from: date))", + performer: nil, + waveform: nil + ) + ], + alternativeRepresentations: [] + ) + + let message: EnqueueMessage = .message( + text: "", + attributes: [], + inlineStickers: [:], + mediaReference: .standalone(media: file), + threadId: nil, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ) + + DispatchQueue.main.async { + if let account = self.accountContext?.account { + let _ = enqueueMessages( + account: account, + peerId: account.peerId, + messages: [message] + ).start(completed: { [weak self] in + self?.deleteFile(from: path) + self?.showRecordSaveToast() + sendCallRecorderAnalytics(with: .end) + completion?() + }) + } + } + } + + private func callActiveState( + with audioDevice: OngoingCallContext.AudioDevice?, + accountContext: AccountContext + ) { + if let audioDevice { + self.audioDevice = audioDevice + self.accountContext = accountContext + } + +// if NGSettings.recordAllCalls { +// sendCallRecorderAnalytics(with: .startAuto) +// startRecordCall { [weak self] in +// self?.callCompletion?() +// } +// } + } + + public func startRecordCall( + with completion: @escaping () -> Void + ) { + audioDevice?.startNicegramRecording(callback: { [weak self] path, duration, size in + self?.writeAudioToSaved( + from: path, + duration: duration, + size: size, + completion: completion + ) + }, errorCallback: { error in + sendCallRecorderAnalytics(with: .error) + }) + sendCallRecorderAnalytics(with: .start) + } + + public func stopRecordCall() { + audioDevice?.stopNicegramRecording() + } + + public func setupPeer(peer: EnginePeer) { + self.enginePeer = peer + } + + public func showRecordSaveToast() { + let (presentationData, _, _) = getDeviceAccessData() + guard let image = UIImage(bundleImageName: "RecordSave") else { return } + + let content: UndoOverlayContent = .image( + image: image, + title: nil, + text: l("NicegramCallRecord.SavedMessage"), + round: true, + undoText: nil + ) + + DispatchQueue.main.async { + let controller = UndoOverlayController( + presentationData: presentationData, + content: content, + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in return false } + ) + + self.accountContext?.sharedContext.mainWindow?.present(controller, on: .root) + } + } + // } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index f270f3ab290..d7f1cfd5166 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -730,6 +730,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public var myAudioLevel: Signal { return self.myAudioLevelPipe.signal() } + private let myAudioLevelAndSpeakingPipe = ValuePipe<(Float, Bool)>() + public var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { + return self.myAudioLevelAndSpeakingPipe.signal() + } private var myAudioLevelDisposable = MetaDisposable() private var audioSessionControl: ManagedAudioSessionControl? @@ -1957,6 +1961,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let mappedLevel = myLevel * 1.5 strongSelf.myAudioLevelPipe.putNext(mappedLevel) + strongSelf.myAudioLevelAndSpeakingPipe.putNext((mappedLevel, myLevelHasVoice)) strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) strongSelf.isSpeakingPromise.set(orignalMyLevelHasVoice) @@ -2105,7 +2110,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var topParticipants: [GroupCallParticipantsContext.Participant] = [] var reportSpeakingParticipants: [PeerId: UInt32] = [:] - let timestamp = CACurrentMediaTime() + let timestamp = CFAbsoluteTimeGetCurrent() for (peerId, ssrc) in speakingParticipants { let shouldReport: Bool if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] { diff --git a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift new file mode 100644 index 00000000000..6ceaf18ebf1 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift @@ -0,0 +1,465 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import TelegramCore +import AnimatedTextComponent +import MultilineTextComponent +import BalancedTextComponent +import TelegramPresentationData +import TelegramStringFormatting +import Markdown + +private final class ScheduleVideoChatSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let scheduleAction: (Int32) -> Void + let dismiss: () -> Void + + init( + scheduleAction: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { + self.scheduleAction = scheduleAction + self.dismiss = dismiss + } + + static func ==(lhs: ScheduleVideoChatSheetContentComponent, rhs: ScheduleVideoChatSheetContentComponent) -> Bool { + return true + } + + final class View: UIView { + private let button = ComponentView() + private let cancelButton = ComponentView() + + private let title = ComponentView() + private let mainText = ComponentView() + private var pickerView: UIDatePicker? + + private let calendar = Calendar(identifier: .gregorian) + private let dateFormatter: DateFormatter + + private var component: ScheduleVideoChatSheetContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.dateFormatter = DateFormatter() + self.dateFormatter.timeStyle = .none + self.dateFormatter.dateStyle = .short + self.dateFormatter.timeZone = TimeZone.current + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func scheduleDatePickerUpdated() { + self.state?.updated(transition: .immediate) + } + + private func updateSchedulePickerLimits() { + let timeZone = TimeZone(secondsFromGMT: 0)! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let currentDate = Date() + var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) + components.second = 0 + + let roundedDate = calendar.date(from: components)! + let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate) + + let minute = components.minute ?? 0 + components.minute = 0 + let roundedToHourDate = calendar.date(from: components)! + components.hour = 0 + + let roundedToMidnightDate = calendar.date(from: components)! + let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate) + let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate) + + if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) { + self.pickerView?.maximumDate = date + } + if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate { + self.pickerView?.minimumDate = next1MinDate + self.pickerView?.maximumDate = maxDate + self.pickerView?.date = nextTwoHourDate + } + } + + func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + let _ = previousComponent + + self.component = component + self.state = state + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 16.0 + + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: environment.strings.VideoChat_ScheduleButtonTitle, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 16.0 + + let pickerView: UIDatePicker + if let current = self.pickerView { + pickerView = current + } else { + let textColor = UIColor.white + UILabel.setDateLabel(textColor) + + pickerView = UIDatePicker() + pickerView.timeZone = TimeZone(secondsFromGMT: 0) + pickerView.datePickerMode = .countDownTimer + pickerView.datePickerMode = .dateAndTime + pickerView.locale = Locale.current + pickerView.timeZone = TimeZone.current + pickerView.minuteInterval = 1 + self.addSubview(pickerView) + pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged) + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(textColor, forKey: "textColor") + self.pickerView = pickerView + self.addSubview(pickerView) + + self.updateSchedulePickerLimits() + } + + let pickerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 216.0)) + transition.setFrame(view: pickerView, frame: pickerFrame) + contentHeight += pickerFrame.height + contentHeight += 26.0 + + let date = pickerView.date + let calendar = Calendar(identifier: .gregorian) + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let timestamp = Int32(date.timeIntervalSince1970) + let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: PresentationDateTimeFormat()) + let buttonTitle: String + if calendar.isDateInToday(date) { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleToday(time).string + } else if calendar.isDateInTomorrow(date) { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleTomorrow(time).string + } else { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).string + } + + let delta = timestamp - currentTimestamp + + let isGroup = "".isEmpty + let intervalString = scheduledTimeIntervalString(strings: environment.strings, value: max(60, delta)) + + let text: String = isGroup ? environment.strings.ScheduleVoiceChat_GroupText(intervalString).string : environment.strings.ScheduleLiveStream_ChannelText(intervalString).string + + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(14.0), + textColor: UIColor(rgb: 0x8e8e93) + ), + bold: MarkdownAttributeSet( + font: Font.semibold(14.0), + textColor: UIColor(rgb: 0x8e8e93) + ), + link: MarkdownAttributeSet( + font: Font.regular(14.0), + textColor: environment.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + contentHeight += 10.0 + + var buttonContents: [AnyComponentWithIdentity] = [] + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + ))) + let buttonTransition = transition + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: UIColor(rgb: 0x3252EF), + foreground: .white, + pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + HStack(buttonContents, spacing: 5.0) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let pickerView = self.pickerView else { + return + } + component.scheduleAction(Int32(pickerView.date.timeIntervalSince1970)) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + contentHeight += 10.0 + + let cancelButtonSize = self.cancelButton.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: UIColor(rgb: 0x2B2B2F), + foreground: .white, + pressedColor: UIColor(rgb: 0x2B2B2F).withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.Common_Cancel, font: Font.semibold(17.0), color: environment.theme.list.itemPrimaryTextColor) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: cancelButtonSize) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + contentHeight += cancelButtonSize.height + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ScheduleVideoChatSheetScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let scheduleAction: (Int32) -> Void + + init( + context: AccountContext, + scheduleAction: @escaping (Int32) -> Void + ) { + self.context = context + self.scheduleAction = scheduleAction + } + + static func ==(lhs: ScheduleVideoChatSheetScreenComponent, rhs: ScheduleVideoChatSheetScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: ScheduleVideoChatSheetScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ScheduleVideoChatSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(ScheduleVideoChatSheetContentComponent( + scheduleAction: { [weak self] timestamp in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self, let component = self.component else { + return + } + if let controller = self.environment?.controller() { + controller.dismiss(completion: nil) + } + + component.scheduleAction(timestamp) + }) + }, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self else { + return + } + if let controller = self.environment?.controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(UIColor(rgb: 0x1C1C1E)), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ScheduleVideoChatSheetScreen: ViewControllerComponentContainer { + public init(context: AccountContext, scheduleAction: @escaping (Int32) -> Void) { + super.init(context: context, component: ScheduleVideoChatSheetScreenComponent( + context: context, + scheduleAction: scheduleAction + ), navigationBarAppearance: .none, theme: .dark) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 13c69b2cf27..beef446dc24 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -36,13 +36,13 @@ final class VideoChatActionButtonComponent: Component { case leave } - case audio(audio: Audio) + case audio(audio: Audio, isEnabled: Bool) case video(isActive: Bool) case leave fileprivate var iconType: IconType { switch self { - case let .audio(audio): + case let .audio(audio, _): let mappedAudio: IconType.Audio switch audio { case .none, .builtin, .speaker: @@ -66,6 +66,7 @@ final class VideoChatActionButtonComponent: Component { case muted case unmuted case raiseHand + case scheduled } let strings: PresentationStrings @@ -135,14 +136,16 @@ final class VideoChatActionButtonComponent: Component { let titleText: String let backgroundColor: UIColor let iconDiameter: CGFloat + var isEnabled: Bool = true switch component.content { - case let .audio(audio): + case let .audio(audio, isEnabledValue): var isActive = false switch audio { case .none, .builtin: titleText = component.strings.Call_Speaker case .speaker: - isActive = true + isEnabled = isEnabledValue + isActive = isEnabledValue titleText = component.strings.Call_Speaker case .headphones: titleText = component.strings.Call_Audio @@ -156,12 +159,12 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF) case .unmuted: backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) - case .raiseHand: - backgroundColor = UIColor(rgb: 0x3252EF) + case .raiseHand, .scheduled: + backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 case let .video(isActive): - titleText = "video" + titleText = component.strings.VoiceChat_Video switch component.microphoneState { case .connecting: backgroundColor = UIColor(white: 0.1, alpha: 1.0) @@ -169,12 +172,12 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF) case .unmuted: backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) - case .raiseHand: + case .raiseHand, .scheduled: backgroundColor = UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 case .leave: - titleText = "leave" + titleText = component.strings.VoiceChat_Leave backgroundColor = UIColor(rgb: 0x47191E) iconDiameter = 22.0 } @@ -275,8 +278,12 @@ final class VideoChatActionButtonComponent: Component { self.addSubview(iconView) } transition.setFrame(view: iconView, frame: iconFrame) + transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6) } + self.isEnabled = isEnabled + self.isUserInteractionEnabled = isEnabled + return size } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index 716bfd8076b..ff93749b666 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -11,6 +11,7 @@ import SwiftSignalKit import MetalEngine import CallScreen import AvatarNode +import ContextUI final class VideoChatParticipantThumbnailComponent: Component { let call: PresentationGroupCall @@ -19,7 +20,9 @@ final class VideoChatParticipantThumbnailComponent: Component { let isPresentation: Bool let isSelected: Bool let isSpeaking: Bool + let interfaceOrientation: UIInterfaceOrientation let action: (() -> Void)? + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: PresentationGroupCall, @@ -28,7 +31,9 @@ final class VideoChatParticipantThumbnailComponent: Component { isPresentation: Bool, isSelected: Bool, isSpeaking: Bool, - action: (() -> Void)? + interfaceOrientation: UIInterfaceOrientation, + action: (() -> Void)?, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme @@ -36,7 +41,9 @@ final class VideoChatParticipantThumbnailComponent: Component { self.isPresentation = isPresentation self.isSelected = isSelected self.isSpeaking = isSpeaking + self.interfaceOrientation = interfaceOrientation self.action = action + self.contextAction = contextAction } static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool { @@ -58,20 +65,31 @@ final class VideoChatParticipantThumbnailComponent: Component { if lhs.isSpeaking != rhs.isSpeaking { return false } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } return true } private struct VideoSpec: Equatable { var resolution: CGSize var rotationAngle: Float + var followsDeviceOrientation: Bool - init(resolution: CGSize, rotationAngle: Float) { + init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) { self.resolution = resolution self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation } } - final class View: HighlightTrackingButton { + final class View: ContextControllerSourceView { private static let selectedBorderImage: UIImage? = { return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate) }() @@ -80,6 +98,10 @@ final class VideoChatParticipantThumbnailComponent: Component { private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false + private let extractedContainerView: ContextExtractedContentContainingView + + private let backgroundLayer: SimpleLayer + private var avatarNode: AvatarNode? private let title = ComponentView() private let muteStatus = ComponentView() @@ -93,13 +115,30 @@ final class VideoChatParticipantThumbnailComponent: Component { private var videoSpec: VideoSpec? override init(frame: CGRect) { + self.extractedContainerView = ContextExtractedContentContainingView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor + super.init(frame: frame) - //TODO:release optimize - self.clipsToBounds = true - self.layer.cornerRadius = 10.0 + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.extractedContainerView.contentView.clipsToBounds = true + self.extractedContainerView.contentView.layer.cornerRadius = 10.0 + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + } } required init?(coder: NSCoder) { @@ -110,11 +149,13 @@ final class VideoChatParticipantThumbnailComponent: Component { self.videoDisposable?.dispose() } - @objc private func pressed() { - guard let component = self.component, let action = component.action else { - return + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let component = self.component, let action = component.action else { + return + } + action() } - action() } func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -123,10 +164,6 @@ final class VideoChatParticipantThumbnailComponent: Component { self.isUpdating = false } - if self.component == nil { - self.backgroundColor = UIColor(rgb: 0x1C1C1E) - } - let previousComponent = self.component let wasSpeaking = previousComponent?.isSpeaking ?? false @@ -148,6 +185,14 @@ final class VideoChatParticipantThumbnailComponent: Component { self.component = component self.componentState = state + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) + + transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) + let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current @@ -155,7 +200,7 @@ final class VideoChatParticipantThumbnailComponent: Component { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) avatarNode.isUserInteractionEnabled = false self.avatarNode = avatarNode - self.addSubview(avatarNode.view) + self.extractedContainerView.contentView.addSubview(avatarNode.view) } let avatarSize = CGSize(width: 50.0, height: 50.0) @@ -180,7 +225,7 @@ final class VideoChatParticipantThumbnailComponent: Component { let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize) if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View { if muteStatusView.superview == nil { - self.addSubview(muteStatusView) + self.extractedContainerView.contentView.addSubview(muteStatusView) } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) @@ -193,14 +238,14 @@ final class VideoChatParticipantThumbnailComponent: Component { text: .plain(NSAttributedString(string: EnginePeer(component.participant.peer).compactDisplayTitle, font: Font.semibold(13.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 8.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 12.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: 6.0, y: availableSize.height - 6.0 - titleSize.height), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() titleView.isUserInteractionEnabled = false - self.addSubview(titleView) + self.extractedContainerView.contentView.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) @@ -214,7 +259,7 @@ final class VideoChatParticipantThumbnailComponent: Component { videoBackgroundLayer = SimpleLayer() videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor self.videoBackgroundLayer = videoBackgroundLayer - self.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) + self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) videoBackgroundLayer.isHidden = true } @@ -224,8 +269,8 @@ final class VideoChatParticipantThumbnailComponent: Component { } else { videoLayer = PrivateCallVideoLayer() self.videoLayer = videoLayer - self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) - self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) + self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) + self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) videoLayer.blurredLayer.opacity = 0.25 @@ -243,7 +288,7 @@ final class VideoChatParticipantThumbnailComponent: Component { videoLayer.video = videoOutput if let videoOutput { - let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle) + let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) if self.videoSpec != videoSpec { self.videoSpec = videoSpec if !self.isUpdating { @@ -269,9 +314,11 @@ final class VideoChatParticipantThumbnailComponent: Component { videoLayer.blurredLayer.isHidden = component.isSelected videoLayer.isHidden = component.isSelected + let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation) + var rotatedResolution = videoSpec.resolution var videoIsRotated = false - if abs(videoSpec.rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(videoSpec.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { + if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { videoIsRotated = true } if videoIsRotated { @@ -303,12 +350,12 @@ final class VideoChatParticipantThumbnailComponent: Component { transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize)) - transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) + transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center) transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize)) - transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) + transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) } } else { if let videoBackgroundLayer = self.videoBackgroundLayer { @@ -336,7 +383,7 @@ final class VideoChatParticipantThumbnailComponent: Component { selectedBorderView = UIImageView() self.selectedBorderView = selectedBorderView selectedBorderView.alpha = 0.0 - self.addSubview(selectedBorderView) + self.extractedContainerView.contentView.addSubview(selectedBorderView) selectedBorderView.image = View.selectedBorderImage selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize) @@ -426,7 +473,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { let participants: [Participant] let selectedParticipant: Participant.Key? let speakingParticipants: Set + let interfaceOrientation: UIInterfaceOrientation let updateSelectedParticipant: (Participant.Key) -> Void + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: PresentationGroupCall, @@ -434,14 +483,18 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { participants: [Participant], selectedParticipant: Participant.Key?, speakingParticipants: Set, - updateSelectedParticipant: @escaping (Participant.Key) -> Void + interfaceOrientation: UIInterfaceOrientation, + updateSelectedParticipant: @escaping (Participant.Key) -> Void, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme self.participants = participants self.selectedParticipant = selectedParticipant self.speakingParticipants = speakingParticipants + self.interfaceOrientation = interfaceOrientation self.updateSelectedParticipant = updateSelectedParticipant + self.contextAction = contextAction } static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool { @@ -460,6 +513,12 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { if lhs.speakingParticipants != rhs.speakingParticipants { return false } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } return true } @@ -595,12 +654,14 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { isPresentation: participant.isPresentation, isSelected: component.selectedParticipant == participant.key, isSpeaking: component.speakingParticipants.contains(participant.participant.peer.id), + interfaceOrientation: component.interfaceOrientation, action: { [weak self] in guard let self, let component = self.component else { return } component.updateSelectedParticipant(participantKey) - } + }, + contextAction: component.contextAction )), environment: {}, containerSize: itemFrame.size diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift new file mode 100644 index 00000000000..d50ae11f7fe --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import AccountContext +import TelegramCore +import Markdown +import TextFormat + +final class VideoChatExpandedSpeakingToastComponent: Component { + let context: AccountContext + let peer: EnginePeer + let strings: PresentationStrings + let theme: PresentationTheme + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + strings: PresentationStrings, + theme: PresentationTheme, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.peer = peer + self.strings = strings + self.theme = theme + self.action = action + } + + static func ==(lhs: VideoChatExpandedSpeakingToastComponent, rhs: VideoChatExpandedSpeakingToastComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let background = ComponentView() + private let title = ComponentView() + private var avatarNode: AvatarNode? + + private var component: VideoChatExpandedSpeakingToastComponent? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + if let component = self.component { + component.action(component.peer) + } + } + + func update(component: VideoChatExpandedSpeakingToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + + let avatarLeftInset: CGFloat = 3.0 + let avatarVerticalInset: CGFloat = 3.0 + let avatarSpacing: CGFloat = 12.0 + let rightInset: CGFloat = 16.0 + let avatarWidth: CGFloat = 32.0 + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:]) + let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:]) + let titleText = addAttributesToStringWithRanges(component.strings.VoiceChat_ParticipantIsSpeaking(component.peer.displayTitle(strings: component.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleText) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarLeftInset - avatarWidth - avatarSpacing - rightInset, height: 100.0) + ) + + let size = CGSize(width: avatarLeftInset + avatarWidth + avatarSpacing + titleSize.width + rightInset, height: avatarWidth + avatarVerticalInset * 2.0) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 0.0, alpha: 0.9), + cornerRadius: .value(size.height * 0.5), + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarWidth + avatarSpacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + avatarNode.isUserInteractionEnabled = false + } + + let avatarSize = CGSize(width: avatarWidth, height: avatarWidth) + + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = component.peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + + if component.peer.smallProfileImage != nil { + avatarNode.setPeerV2( + context: component.context, + theme: component.theme, + peer: component.peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: false, + displayDimensions: avatarSize + ) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: avatarVerticalInset), size: avatarSize) + transition.setPosition(view: avatarNode.view, position: avatarFrame.center) + transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + avatarNode.updateSize(size: avatarSize) + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index 9e7c87b5bba..1541bf5ff97 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -175,31 +175,43 @@ private final class GlowView: UIView { } final class VideoChatMicButtonComponent: Component { + enum ScheduledState: Equatable { + case start + case toggleSubscription(isSubscribed: Bool) + } + enum Content: Equatable { case connecting case muted case unmuted(pushToTalk: Bool) - case raiseHand + case raiseHand(isRaised: Bool) + case scheduled(state: ScheduledState) } let call: PresentationGroupCall + let strings: PresentationStrings let content: Content let isCollapsed: Bool let updateUnmutedStateIsPushToTalk: (Bool?) -> Void let raiseHand: () -> Void + let scheduleAction: () -> Void init( call: PresentationGroupCall, + strings: PresentationStrings, content: Content, isCollapsed: Bool, updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void, - raiseHand: @escaping () -> Void + raiseHand: @escaping () -> Void, + scheduleAction: @escaping () -> Void ) { self.call = call + self.strings = strings self.content = content self.isCollapsed = isCollapsed self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk self.raiseHand = raiseHand + self.scheduleAction = scheduleAction } static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool { @@ -217,6 +229,7 @@ final class VideoChatMicButtonComponent: Component { private var disappearingBackgrounds: [UIImageView] = [] private var progressIndicator: RadialStatusNode? private let title = ComponentView() + private var subtitle: ComponentView? private let icon: VoiceChatActionButtonIconNode private var glowView: GlowView? @@ -245,7 +258,7 @@ final class VideoChatMicButtonComponent: Component { self.beginTrackingTimestamp = CFAbsoluteTimeGetCurrent() if let component = self.component { switch component.content { - case .connecting, .unmuted, .raiseHand: + case .connecting, .unmuted, .raiseHand, .scheduled: self.beginTrackingWasPushToTalk = false case .muted: self.beginTrackingWasPushToTalk = true @@ -291,6 +304,8 @@ final class VideoChatMicButtonComponent: Component { self.icon.playRandomAnimation() component.raiseHand() + case .scheduled: + component.scheduleAction() } } } @@ -311,17 +326,35 @@ final class VideoChatMicButtonComponent: Component { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) let titleText: String + var subtitleText: String? var isEnabled = true switch component.content { case .connecting: - titleText = "Connecting..." + titleText = component.strings.VoiceChat_Connecting isEnabled = false case .muted: - titleText = "Unmute" + titleText = component.strings.VoiceChat_Unmute case let .unmuted(isPushToTalk): - titleText = isPushToTalk ? "You are Live" : "Tap to Mute" - case .raiseHand: - titleText = "Raise Hand" + titleText = isPushToTalk ? component.strings.VoiceChat_Live : component.strings.VoiceChat_Mute + case let .raiseHand(isRaised): + if isRaised { + titleText = component.strings.VoiceChat_AskedToSpeak + subtitleText = component.strings.VoiceChat_AskedToSpeakHelp + } else { + titleText = component.strings.VoiceChat_MutedByAdmin + subtitleText = component.strings.VoiceChat_MutedByAdminHelp + } + case let .scheduled(state): + switch state { + case .start: + titleText = component.strings.VoiceChat_StartNow + case let .toggleSubscription(isSubscribed): + if isSubscribed { + titleText = component.strings.VoiceChat_CancelReminder + } else { + titleText = component.strings.VoiceChat_SetReminder + } + } } self.isEnabled = isEnabled @@ -331,7 +364,7 @@ final class VideoChatMicButtonComponent: Component { text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) + containerSize: CGSize(width: 180.0, height: 100.0) ) let size = CGSize(width: availableSize.width, height: availableSize.height) @@ -390,12 +423,14 @@ final class VideoChatMicButtonComponent: Component { case .connecting: context.setFillColor(UIColor(white: 0.1, alpha: 1.0).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - case .muted, .unmuted, .raiseHand: + case .muted, .unmuted, .raiseHand, .scheduled: let colors: [UIColor] if case .muted = component.content { colors = [UIColor(rgb: 0x0080FF), UIColor(rgb: 0x00A1FE)] } else if case .raiseHand = component.content { colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)] + } else if case .scheduled = component.content { + colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)] } else { colors = [UIColor(rgb: 0x33C659), UIColor(rgb: 0x0BA8A5)] } @@ -446,7 +481,10 @@ final class VideoChatMicButtonComponent: Component { transition.setScale(view: disappearingBackground, scale: size.width / 116.0) } - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize) + var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize) + if subtitleText != nil { + titleFrame.origin.y -= 5.0 + } if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -457,6 +495,47 @@ final class VideoChatMicButtonComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0) } + if let subtitleText { + let subtitle: ComponentView + var subtitleTransition = transition + if let current = self.subtitle { + subtitle = current + } else { + subtitleTransition = subtitleTransition.withAnimation(.none) + subtitle = ComponentView() + self.subtitle = subtitle + } + let subtitleSize = subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 180.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + 1.0), size: subtitleSize) + if let subtitleView = subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.addSubview(subtitleView) + + subtitleView.alpha = 0.0 + transition.animateScale(view: subtitleView, from: 0.001, to: 1.0) + } + subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + alphaTransition.setAlpha(view: subtitleView, alpha: component.isCollapsed ? 0.0 : 1.0) + } + } else if let subtitle = self.subtitle { + self.subtitle = nil + if let subtitleView = subtitle.view { + transition.setScale(view: subtitleView, scale: 0.001) + alphaTransition.setAlpha(view: subtitleView, alpha: 0.0, completion: { [weak subtitleView] _ in + subtitleView?.removeFromSuperview() + }) + } + } + if self.icon.view.superview == nil { self.icon.view.isUserInteractionEnabled = false self.addSubview(self.icon.view) @@ -477,10 +556,21 @@ final class VideoChatMicButtonComponent: Component { self.icon.enqueueState(.unmute) case .raiseHand: self.icon.enqueueState(.hand) + case let .scheduled(state): + switch state { + case .start: + self.icon.enqueueState(.start) + case let .toggleSubscription(isSubscribed): + if isSubscribed { + self.icon.enqueueState(.unsubscribe) + } else { + self.icon.enqueueState(.subscribe) + } + } } switch component.content { - case .muted, .unmuted, .raiseHand: + case .muted, .unmuted, .raiseHand, .scheduled: let blobSize = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0)).insetBy(dx: -40.0, dy: -40.0).size let blobTintTransition: ComponentTransition @@ -512,6 +602,8 @@ final class VideoChatMicButtonComponent: Component { blobsColor = UIColor(rgb: 0x0086FF) } else if case .raiseHand = component.content { blobsColor = UIColor(rgb: 0x914BAD) + } else if case .scheduled = component.content { + blobsColor = UIColor(rgb: 0x914BAD) } else { blobsColor = UIColor(rgb: 0x33C758) } @@ -528,7 +620,7 @@ final class VideoChatMicButtonComponent: Component { blobView.updateLevel(CGFloat(value), immediately: false) }) } - case .connecting, .muted, .raiseHand: + case .connecting, .muted, .raiseHand, .scheduled: if let audioLevelDisposable = self.audioLevelDisposable { self.audioLevelDisposable = nil audioLevelDisposable.dispose() @@ -561,6 +653,8 @@ final class VideoChatMicButtonComponent: Component { glowColor = UIColor(rgb: 0x0086FF) } else if case .raiseHand = component.content { glowColor = UIColor(rgb: 0x3252EF) + } else if case .scheduled = component.content { + glowColor = UIColor(rgb: 0x3252EF) } else { glowColor = UIColor(rgb: 0x33C758) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift index 3bc92c4bcf2..748a319b13f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift @@ -16,13 +16,19 @@ final class VideoChatMuteIconComponent: Component { let color: UIColor let content: Content + let shadowColor: UIColor? + let shadowBlur: CGFloat init( color: UIColor, - content: Content + content: Content, + shadowColor: UIColor? = nil, + shadowBlur: CGFloat = 0.0 ) { self.color = color self.content = content + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool { @@ -32,6 +38,12 @@ final class VideoChatMuteIconComponent: Component { if lhs.content != rhs.content { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } @@ -75,9 +87,9 @@ final class VideoChatMuteIconComponent: Component { } let animationSize = availableSize - let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) + let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)).insetBy(dx: -component.shadowBlur, dy: -component.shadowBlur) transition.setFrame(view: icon.view, frame: animationFrame) - icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate) + icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color, shadowColor: component.shadowColor, shadowBlur: component.shadowBlur), animated: !transition.animation.isImmediate) } else { if let icon = self.icon { self.icon = nil @@ -97,7 +109,9 @@ final class VideoChatMuteIconComponent: Component { transition: transition, component: AnyComponent(BundleIconComponent( name: "Call/StatusScreen", - tintColor: component.color + tintColor: component.color, + shadowColor: component.shadowColor, + shadowBlur: component.shadowBlur )), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index bc631b0f845..834fc033183 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -97,20 +97,17 @@ private final class BlobView: UIView { } private func updateAudioLevel() { - let additionalAvatarScale = CGFloat(max(0.0, min(self.presentationAudioLevel * 18.0, 5.0)) * 0.05) - let blobAmplificationFactor: CGFloat = 2.0 - let blobScale = 1.0 + additionalAvatarScale * blobAmplificationFactor + let additionalAvatarScale = CGFloat(max(0.0, min(self.presentationAudioLevel * 0.3, 1.0)) * 1.0) + let blobScale = 1.28 + additionalAvatarScale self.blobsLayer.transform = CATransform3DMakeScale(blobScale, blobScale, 1.0) - self.scaleUpdated?(blobScale) + self.scaleUpdated?(additionalAvatarScale) } public func startAnimating() { guard !self.isAnimating else { return } self.isAnimating = true - self.updateBlobsState() - self.displayLinkAnimator?.isPaused = false } @@ -122,51 +119,35 @@ private final class BlobView: UIView { guard isAnimating else { return } self.isAnimating = false - self.updateBlobsState() - self.displayLinkAnimator?.isPaused = true } - private func updateBlobsState() { - /*if self.isAnimating { - if self.mediumBlob.frame.size != .zero { - self.mediumBlob.startAnimating() - self.bigBlob.startAnimating() - } - } else { - self.mediumBlob.stopAnimating() - self.bigBlob.stopAnimating() - }*/ - } - - override public func layoutSubviews() { + func update(size: CGSize) { super.layoutSubviews() - //self.mediumBlob.frame = bounds - //self.bigBlob.frame = bounds - - let blobsFrame = bounds.insetBy(dx: floor(bounds.width * 0.12), dy: floor(bounds.height * 0.12)) + let blobsFrame = CGRect(origin: CGPoint(), size: size) self.blobsLayer.position = blobsFrame.center self.blobsLayer.bounds = CGRect(origin: CGPoint(), size: blobsFrame.size) - - self.updateBlobsState() } } final class VideoChatParticipantAvatarComponent: Component { let call: PresentationGroupCall let peer: EnginePeer + let myPeerId: EnginePeer.Id let isSpeaking: Bool let theme: PresentationTheme init( call: PresentationGroupCall, peer: EnginePeer, + myPeerId: EnginePeer.Id, isSpeaking: Bool, theme: PresentationTheme ) { self.call = call self.peer = peer + self.myPeerId = myPeerId self.isSpeaking = isSpeaking self.theme = theme } @@ -181,6 +162,9 @@ final class VideoChatParticipantAvatarComponent: Component { if lhs.isSpeaking != rhs.isSpeaking { return false } + if lhs.myPeerId != rhs.myPeerId { + return false + } if lhs.theme !== rhs.theme { return false } @@ -197,6 +181,7 @@ final class VideoChatParticipantAvatarComponent: Component { private var wasSpeaking: Bool? private var noAudioTimer: Foundation.Timer? + private var lastAudioLevelTimestamp: Double = 0.0 override init(frame: CGRect) { super.init(frame: frame) @@ -211,6 +196,31 @@ final class VideoChatParticipantAvatarComponent: Component { self.noAudioTimer?.invalidate() } + private func checkNoAudio() { + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastAudioLevelTimestamp + 1.0 < timestamp { + self.noAudioTimer?.invalidate() + self.noAudioTimer = nil + + if let blobView = self.blobView { + let transition: ComponentTransition = .easeInOut(duration: 0.3) + transition.setAlpha(view: blobView, alpha: 0.0, completion: { [weak self, weak blobView] completed in + guard let self, let blobView, completed else { + return + } + if self.blobView === blobView { + self.blobView = nil + } + blobView.removeFromSuperview() + }) + transition.setScale(layer: blobView.layer, scale: 0.5) + if let avatarNode = self.avatarNode { + transition.setScale(view: avatarNode.view, scale: 1.0) + } + } + } + } + func update(component: VideoChatParticipantAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -268,39 +278,60 @@ final class VideoChatParticipantAvatarComponent: Component { avatarNode.setPeer(context: component.call.accountContext, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize) } - transition.setFrame(view: avatarNode.view, frame: CGRect(origin: CGPoint(), size: avatarSize)) + let avatarFrame = CGRect(origin: CGPoint(), size: avatarSize) + transition.setPosition(view: avatarNode.view, position: avatarFrame.center) + transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) avatarNode.updateSize(size: avatarSize) + let blobScale: CGFloat = 2.0 + if self.audioLevelDisposable == nil { - let peerId = component.peer.id struct Level { var value: Float var isSpeaking: Bool } - self.audioLevelDisposable = (component.call.audioLevels - |> map { levels -> Level? in - for level in levels { - if level.0 == peerId { - return Level(value: level.2, isSpeaking: level.3) + + let peerId = component.peer.id + let levelSignal: Signal + if peerId == component.myPeerId { + levelSignal = component.call.myAudioLevelAndSpeaking + |> map { value, isSpeaking -> Level? in + if value == 0.0 { + return nil + } else { + return Level(value: value, isSpeaking: isSpeaking) } } - return nil + } else { + levelSignal = component.call.audioLevels + |> map { levels -> Level? in + for level in levels { + if level.0 == peerId { + return Level(value: level.2, isSpeaking: level.3) + } + } + return nil + } } + + self.audioLevelDisposable = (levelSignal |> distinctUntilChanged(isEqual: { lhs, rhs in if (lhs == nil) != (rhs == nil) { return false } if lhs != nil { - return true - } else { return false + } else { + return true } }) |> deliverOnMainQueue).startStrict(next: { [weak self] level in guard let self, let component = self.component, let avatarNode = self.avatarNode else { return } - if let level { + if let level, level.value >= 0.1 { + self.lastAudioLevelTimestamp = CFAbsoluteTimeGetCurrent() + let blobView: BlobView if let current = self.blobView { blobView = current @@ -314,14 +345,31 @@ final class VideoChatParticipantAvatarComponent: Component { bigBlobRange: (0.71, 1.0) ) self.blobView = blobView - blobView.frame = avatarNode.frame + let blobSize = floor(avatarNode.bounds.width * blobScale) + blobView.center = avatarNode.frame.center + blobView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: blobSize, height: blobSize)) + blobView.layer.transform = CATransform3DMakeScale(1.0 / blobScale, 1.0 / blobScale, 1.0) + + blobView.update(size: blobView.bounds.size) self.insertSubview(blobView, belowSubview: avatarNode.view) - blobView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + blobView.layer.animateScale(from: 0.5, to: 1.0 / blobScale, duration: 0.2) + + blobView.scaleUpdated = { [weak self] additionalScale in + guard let self, let avatarNode = self.avatarNode else { + return + } + avatarNode.layer.transform = CATransform3DMakeScale(1.0 + additionalScale, 1.0 + additionalScale, 1.0) + } ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } + if blobView.alpha == 0.0 { + let transition: ComponentTransition = .easeInOut(duration: 0.3) + transition.setAlpha(view: blobView, alpha: 1.0) + transition.setScale(view: blobView, scale: 1.0 / blobScale) + } blobView.updateLevel(CGFloat(level.value), immediately: false) if let noAudioTimer = self.noAudioTimer { @@ -329,24 +377,19 @@ final class VideoChatParticipantAvatarComponent: Component { noAudioTimer.invalidate() } } else { - if self.noAudioTimer == nil { - self.noAudioTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false, block: { [weak self] _ in - guard let self else { - return - } - self.noAudioTimer?.invalidate() - self.noAudioTimer = nil - - if let blobView = self.blobView { - self.blobView = nil - blobView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak blobView] _ in - blobView?.removeFromSuperview() - }) - blobView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) - } - }) + if let blobView = self.blobView { + blobView.updateLevel(0.0, immediately: false) } } + + if self.noAudioTimer == nil { + self.noAudioTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.checkNoAudio() + }) + } }) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index cde0ff9ad99..fa08eaca1a8 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -12,6 +12,9 @@ import AccountContext import SwiftSignalKit import DirectMediaImageCache import FastBlur +import ContextUI +import ComponentDisplayAdapters +import AvatarNode private func blurredAvatarImage(_ dataImage: UIImage) -> UIImage? { let imageContextSize = CGSize(width: 64.0, height: 64.0) @@ -35,8 +38,11 @@ private let activityBorderImage: UIImage = { }() final class VideoChatParticipantVideoComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings let call: PresentationGroupCall let participant: GroupCallParticipantsContext.Participant + let isMyPeer: Bool let isPresentation: Bool let isSpeaking: Bool let isExpanded: Bool @@ -44,12 +50,17 @@ final class VideoChatParticipantVideoComponent: Component { let contentInsets: UIEdgeInsets let controlInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation - weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? let action: (() -> Void)? + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? + let activatePinch: ((PinchSourceContainerNode) -> Void)? + let deactivatedPinch: (() -> Void)? init( + theme: PresentationTheme, + strings: PresentationStrings, call: PresentationGroupCall, participant: GroupCallParticipantsContext.Participant, + isMyPeer: Bool, isPresentation: Bool, isSpeaking: Bool, isExpanded: Bool, @@ -57,11 +68,16 @@ final class VideoChatParticipantVideoComponent: Component { contentInsets: UIEdgeInsets, controlInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, - rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?, - action: (() -> Void)? + action: (() -> Void)?, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?, + activatePinch: ((PinchSourceContainerNode) -> Void)?, + deactivatedPinch: (() -> Void)? ) { + self.theme = theme + self.strings = strings self.call = call self.participant = participant + self.isMyPeer = isMyPeer self.isPresentation = isPresentation self.isSpeaking = isSpeaking self.isExpanded = isExpanded @@ -69,14 +85,19 @@ final class VideoChatParticipantVideoComponent: Component { self.contentInsets = contentInsets self.controlInsets = controlInsets self.interfaceOrientation = interfaceOrientation - self.rootVideoLoadingEffectView = rootVideoLoadingEffectView self.action = action + self.contextAction = contextAction + self.activatePinch = activatePinch + self.deactivatedPinch = deactivatedPinch } static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool { if lhs.participant != rhs.participant { return false } + if lhs.isMyPeer != rhs.isMyPeer { + return false + } if lhs.isPresentation != rhs.isPresentation { return false } @@ -101,6 +122,15 @@ final class VideoChatParticipantVideoComponent: Component { if (lhs.action == nil) != (rhs.action == nil) { return false } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } + if (lhs.activatePinch == nil) != (rhs.activatePinch == nil) { + return false + } + if (lhs.deactivatedPinch == nil) != (rhs.deactivatedPinch == nil) { + return false + } return true } @@ -116,36 +146,97 @@ final class VideoChatParticipantVideoComponent: Component { } } - final class View: HighlightTrackingButton { + private struct ReferenceLocation: Equatable { + var containerWidth: CGFloat + var positionX: CGFloat + + init(containerWidth: CGFloat, positionX: CGFloat) { + self.containerWidth = containerWidth + self.positionX = positionX + } + } + + private final class AnimationHint { + enum Kind { + case videoAvailabilityChanged + } + + let kind: Kind + + init(kind: Kind) { + self.kind = kind + } + } + + final class View: ContextControllerSourceView { private var component: VideoChatParticipantVideoComponent? private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false private var previousSize: CGSize? + private let backgroundGradientView: UIImageView + private let muteStatus = ComponentView() private let title = ComponentView() private var blurredAvatarDisposable: Disposable? private var blurredAvatarView: UIImageView? + private let pinchContainerNode: PinchSourceContainerNode + private let extractedContainerView: ContextExtractedContentContainingView private var videoSource: AdaptedCallVideoSource? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? private var videoLayer: PrivateCallVideoLayer? private var videoSpec: VideoSpec? + private var awaitingFirstVideoFrameForUnpause: Bool = false + private var videoStatus: ComponentView? private var activityBorderView: UIImageView? - private var loadingEffectView: PortalView? + private var referenceLocation: ReferenceLocation? + private var loadingEffectView: VideoChatVideoLoadingEffectView? override init(frame: CGRect) { + self.backgroundGradientView = UIImageView() + self.pinchContainerNode = PinchSourceContainerNode() + self.extractedContainerView = ContextExtractedContentContainingView() + super.init(frame: frame) + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view) + self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView) + //TODO:release optimize - self.clipsToBounds = true - self.layer.cornerRadius = 10.0 + self.pinchContainerNode.contentNode.view.layer.cornerRadius = 10.0 + self.pinchContainerNode.contentNode.view.clipsToBounds = true + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let self, let component = self.component else { + return + } + component.activatePinch?(sourceNode) + } + self.pinchContainerNode.animatedOut = { [weak self] in + guard let self, let component = self.component else { + return + } + + component.deactivatedPinch?() + } + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + } } required init?(coder: NSCoder) { @@ -157,11 +248,13 @@ final class VideoChatParticipantVideoComponent: Component { self.blurredAvatarDisposable?.dispose() } - @objc private func pressed() { - guard let component = self.component, let action = component.action else { - return + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let component = self.component, let action = component.action else { + return + } + action() } - action() } func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -170,9 +263,27 @@ final class VideoChatParticipantVideoComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.componentState = state + self.isGestureEnabled = !component.isExpanded + + self.pinchContainerNode.isPinchGestureEnabled = component.activatePinch != nil + transition.setPosition(view: self.pinchContainerNode.view, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.pinchContainerNode.view, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.pinchContainerNode.update(size: availableSize, transition: transition.containedViewLayoutTransition) + + transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) + + transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + + transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize)) + let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.2) @@ -180,11 +291,20 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition = .immediate } + let videoAlphaTransition: ComponentTransition + if let animationHint = transition.userData(AnimationHint.self), case .videoAvailabilityChanged = animationHint.kind { + videoAlphaTransition = .easeInOut(duration: 0.2) + } else { + videoAlphaTransition = alphaTransition + } + let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0 - let nameColor = component.participant.peer.nameColor ?? .blue - let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) - self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4) + if previousComponent == nil { + let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer.id, nameColor: component.participant.peer.nameColor, icon: .none, theme: component.theme) + + self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical) + } if let smallProfileImage = component.participant.peer.smallProfileImage { let blurredAvatarView: UIImageView @@ -196,7 +316,7 @@ final class VideoChatParticipantVideoComponent: Component { blurredAvatarView = UIImageView() blurredAvatarView.contentMode = .scaleAspectFill self.blurredAvatarView = blurredAvatarView - self.insertSubview(blurredAvatarView, at: 0) + self.pinchContainerNode.contentNode.view.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView) blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize) } @@ -239,7 +359,9 @@ final class VideoChatParticipantVideoComponent: Component { transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, - content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking) + content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking), + shadowColor: UIColor(white: 0.0, alpha: 0.7), + shadowBlur: 8.0 )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) @@ -252,7 +374,7 @@ final class VideoChatParticipantVideoComponent: Component { } if let muteStatusView = self.muteStatus.view { if muteStatusView.superview == nil { - self.addSubview(muteStatusView) + self.pinchContainerNode.contentNode.view.addSubview(muteStatusView) muteStatusView.alpha = controlsAlpha } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) @@ -261,24 +383,28 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha) } + let titleInnerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)) + text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)), + insets: titleInnerInsets, + textShadowColor: UIColor(white: 0.0, alpha: 0.7), + textShadowBlur: 8.0 )), environment: {}, - containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0) ) let titleFrame: CGRect if component.isExpanded { - titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 36.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height + titleInnerInsets.top), size: titleSize) } else { - titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 29.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height + titleInnerInsets.top + 1.0), size: titleSize) } if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() - self.addSubview(titleView) + self.pinchContainerNode.contentNode.view.addSubview(titleView) titleView.alpha = controlsAlpha } transition.setPosition(view: titleView, position: titleFrame.origin) @@ -287,18 +413,34 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } - if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { + let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription + + var isEffectivelyPaused = false + if let videoDescription, videoDescription.isPaused { + isEffectivelyPaused = true + } else if let previousComponent { + let previousVideoDescription = previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription + if let previousVideoDescription, previousVideoDescription.isPaused { + self.awaitingFirstVideoFrameForUnpause = true + } + if self.awaitingFirstVideoFrameForUnpause { + isEffectivelyPaused = true + } + } + + if let videoDescription { let videoBackgroundLayer: SimpleLayer if let current = self.videoBackgroundLayer { videoBackgroundLayer = current } else { videoBackgroundLayer = SimpleLayer() videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor + videoBackgroundLayer.opacity = 0.0 self.videoBackgroundLayer = videoBackgroundLayer if let blurredAvatarView = self.blurredAvatarView { - self.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) } else { - self.layer.insertSublayer(videoBackgroundLayer, at: 0) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer) } videoBackgroundLayer.isHidden = true } @@ -309,10 +451,11 @@ final class VideoChatParticipantVideoComponent: Component { } else { videoLayer = PrivateCallVideoLayer() self.videoLayer = videoLayer - self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) - self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) + videoLayer.opacity = 0.0 + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) - videoLayer.blurredLayer.opacity = 0.25 + videoLayer.blurredLayer.opacity = 0.0 if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) { let videoSource = AdaptedCallVideoSource(videoStreamSignal: input) @@ -329,10 +472,12 @@ final class VideoChatParticipantVideoComponent: Component { if let videoOutput { let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) - if self.videoSpec != videoSpec { + if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { + self.awaitingFirstVideoFrameForUnpause = false + self.videoSpec = videoSpec if !self.isUpdating { - self.componentState?.updated(transition: .immediate, isLocal: true) + self.componentState?.updated(transition: ComponentTransition.immediate.withUserData(AnimationHint(kind: .videoAvailabilityChanged)), isLocal: true) } } } else { @@ -350,7 +495,19 @@ final class VideoChatParticipantVideoComponent: Component { transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { - videoBackgroundLayer.isHidden = false + if videoBackgroundLayer.isHidden { + videoBackgroundLayer.isHidden = false + } + + videoAlphaTransition.setAlpha(layer: videoBackgroundLayer, alpha: 1.0) + + if isEffectivelyPaused { + videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 0.0) + videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.9) + } else { + videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 1.0) + videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.25) + } let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation) @@ -410,17 +567,69 @@ final class VideoChatParticipantVideoComponent: Component { self.videoSpec = nil } - if self.loadingEffectView == nil, let rootVideoLoadingEffectView = component.rootVideoLoadingEffectView { - if let loadingEffectView = PortalView(matchPosition: true) { - self.loadingEffectView = loadingEffectView - self.addSubview(loadingEffectView.view) - rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView) - loadingEffectView.view.isUserInteractionEnabled = false - loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize) + var statusKind: VideoChatParticipantVideoStatusComponent.Kind? + if component.isPresentation && component.isMyPeer { + statusKind = .ownScreenshare + } else if isEffectivelyPaused { + statusKind = .paused + } + + if let statusKind { + let videoStatus: ComponentView + var videoStatusTransition = transition + if let current = self.videoStatus { + videoStatus = current + } else { + videoStatusTransition = videoStatusTransition.withAnimation(.none) + videoStatus = ComponentView() + self.videoStatus = videoStatus + } + let _ = videoStatus.update( + transition: videoStatusTransition, + component: AnyComponent(VideoChatParticipantVideoStatusComponent( + strings: component.strings, + kind: statusKind, + isExpanded: component.isExpanded + )), + environment: {}, + containerSize: availableSize + ) + if let videoStatusView = videoStatus.view { + if videoStatusView.superview == nil { + videoStatusView.isUserInteractionEnabled = false + videoStatusView.alpha = 0.0 + self.pinchContainerNode.contentNode.view.addSubview(videoStatusView) + } + videoStatusTransition.setFrame(view: videoStatusView, frame: CGRect(origin: CGPoint(), size: availableSize)) + videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 1.0) + } + } else if let videoStatus = self.videoStatus { + self.videoStatus = nil + if let videoStatusView = videoStatus.view { + videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 0.0, completion: { [weak videoStatusView] _ in + videoStatusView?.removeFromSuperview() + }) } } - if let loadingEffectView = self.loadingEffectView { - transition.setFrame(view: loadingEffectView.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + + if videoDescription != nil && self.videoSpec == nil && !isEffectivelyPaused { + if self.loadingEffectView == nil { + let loadingEffectView = VideoChatVideoLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, cornerRadius: 10.0, duration: 1.0) + self.loadingEffectView = loadingEffectView + loadingEffectView.alpha = 0.0 + loadingEffectView.isUserInteractionEnabled = false + self.pinchContainerNode.contentNode.view.addSubview(loadingEffectView) + if let referenceLocation = self.referenceLocation { + self.updateHorizontalReferenceLocation(containerWidth: referenceLocation.containerWidth, positionX: referenceLocation.positionX, transition: .immediate) + } + videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 1.0) + } + } else if let loadingEffectView = self.loadingEffectView { + self.loadingEffectView = nil + + videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 0.0, completion: { [weak loadingEffectView] _ in + loadingEffectView?.removeFromSuperview() + }) } if component.isSpeaking && !component.isExpanded { @@ -430,7 +639,7 @@ final class VideoChatParticipantVideoComponent: Component { } else { activityBorderView = UIImageView() self.activityBorderView = activityBorderView - self.addSubview(activityBorderView) + self.pinchContainerNode.contentNode.view.addSubview(activityBorderView) activityBorderView.image = activityBorderImage activityBorderView.tintColor = UIColor(rgb: 0x33C758) @@ -467,6 +676,15 @@ final class VideoChatParticipantVideoComponent: Component { return availableSize } + + func updateHorizontalReferenceLocation(containerWidth: CGFloat, positionX: CGFloat, transition: ComponentTransition) { + self.referenceLocation = ReferenceLocation(containerWidth: containerWidth, positionX: positionX) + + if let loadingEffectView = self.loadingEffectView, let size = self.previousSize { + transition.setFrame(view: loadingEffectView, frame: CGRect(origin: CGPoint(), size: size)) + loadingEffectView.update(size: size, containerWidth: containerWidth, offsetX: positionX, gradientWidth: floor(containerWidth * 0.8), transition: transition) + } + } } func makeView() -> View { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift new file mode 100644 index 00000000000..f8f91cc9eec --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift @@ -0,0 +1,140 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import BundleIconComponent +import MultilineTextComponent + +final class VideoChatParticipantVideoStatusComponent: Component { + enum Kind { + case ownScreenshare + case paused + } + + let strings: PresentationStrings + let kind: Kind + let isExpanded: Bool + + init( + strings: PresentationStrings, + kind: Kind, + isExpanded: Bool + ) { + self.strings = strings + self.kind = kind + self.isExpanded = isExpanded + } + + static func ==(lhs: VideoChatParticipantVideoStatusComponent, rhs: VideoChatParticipantVideoStatusComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.kind != rhs.kind { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private var icon = ComponentView() + private let title = ComponentView() + + private var component: VideoChatParticipantVideoStatusComponent? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: VideoChatParticipantVideoStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousComponent = self.component + self.component = component + + var iconTransition = transition + if let previousComponent, previousComponent.kind != component.kind { + self.icon.view?.removeFromSuperview() + self.icon = ComponentView() + iconTransition = iconTransition.withAnimation(.none) + } + + let iconName: String + let titleValue: String + switch component.kind { + case .ownScreenshare: + iconName = "Call/ScreenSharePhone" + titleValue = component.strings.VoiceChat_YouAreSharingScreen + case .paused: + iconName = "Call/Pause" + titleValue = component.strings.VoiceChat_VideoPaused + } + + let iconSize = self.icon.update( + transition: iconTransition, + component: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: .white + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleValue, font: Font.semibold(14.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0) + ) + + let scale: CGFloat = component.isExpanded ? 1.0 : 0.825 + + let spacing: CGFloat = 18.0 + let contentHeight: CGFloat = iconSize.height + spacing + titleSize.height + + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconTransition.setFrame(view: iconView, frame: iconFrame) + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + iconTransition.setFrame(view: titleView, frame: titleFrame) + } + + iconTransition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0)) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index f4fe335ade4..5d8605d1ef3 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -10,6 +10,7 @@ import SwiftSignalKit import MultilineTextComponent import TelegramPresentationData import PeerListItemComponent +import ContextUI final class VideoChatParticipantsComponent: Component { struct Layout: Equatable { @@ -26,11 +27,13 @@ final class VideoChatParticipantsComponent: Component { var videoColumn: Column? var mainColumn: Column var columnSpacing: CGFloat + var isMainColumnHidden: Bool - init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat) { + init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat, isMainColumnHidden: Bool) { self.videoColumn = videoColumn self.mainColumn = mainColumn self.columnSpacing = columnSpacing + self.isMainColumnHidden = isMainColumnHidden } } @@ -115,6 +118,13 @@ final class VideoChatParticipantsComponent: Component { } } + final class EventCycleState { + var ignoreScrolling: Bool = false + + init() { + } + } + let call: PresentationGroupCall let participants: Participants? let speakingParticipants: Set @@ -126,10 +136,11 @@ final class VideoChatParticipantsComponent: Component { let safeInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void - let updateMainParticipant: (VideoParticipantKey?) -> Void + let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void let openInviteMembers: () -> Void + let visibleParticipantsUpdated: (Set) -> Void init( call: PresentationGroupCall, @@ -143,10 +154,11 @@ final class VideoChatParticipantsComponent: Component { safeInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void, - updateMainParticipant: @escaping (VideoParticipantKey?) -> Void, + updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, - openInviteMembers: @escaping () -> Void + openInviteMembers: @escaping () -> Void, + visibleParticipantsUpdated: @escaping (Set) -> Void ) { self.call = call self.participants = participants @@ -163,6 +175,7 @@ final class VideoChatParticipantsComponent: Component { self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsExpandedUIHidden = updateIsExpandedUIHidden self.openInviteMembers = openInviteMembers + self.visibleParticipantsUpdated = visibleParticipantsUpdated } static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { @@ -422,7 +435,11 @@ final class VideoChatParticipantsComponent: Component { let gridSideInset: CGFloat let gridContainerHeight: CGFloat if let videoColumn = layout.videoColumn { - gridWidth = videoColumn.width + if layout.isMainColumnHidden { + gridWidth = videoColumn.width + layout.columnSpacing + layout.mainColumn.width + } else { + gridWidth = videoColumn.width + } gridSideInset = videoColumn.insets.left gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom } else { @@ -435,7 +452,7 @@ final class VideoChatParticipantsComponent: Component { self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight) self.spacing = 4.0 - if let videoColumn = layout.videoColumn, !isUIHidden { + if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden { self.expandedGrid = ExpandedGrid(containerSize: CGSize(width: videoColumn.width + expandedInsets.left, height: containerSize.height), layout: layout, expandedInsets: UIEdgeInsets(top: expandedInsets.top, left: expandedInsets.left, bottom: expandedInsets.bottom, right: 0.0), isUIHidden: isUIHidden) } else { self.expandedGrid = ExpandedGrid(containerSize: containerSize, layout: layout, expandedInsets: expandedInsets, isUIHidden: isUIHidden) @@ -459,8 +476,8 @@ final class VideoChatParticipantsComponent: Component { var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) - if isUIHidden { - listFrame.origin.x += columnsSideInset + layout.mainColumn.width + if isUIHidden || layout.isMainColumnHidden { + listFrame.origin.x = containerSize.width + columnsSideInset separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height)) } @@ -471,7 +488,7 @@ final class VideoChatParticipantsComponent: Component { self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) } else { self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) - self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom)) + self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX + layout.mainColumn.insets.left, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth - layout.mainColumn.insets.left - layout.mainColumn.insets.right, height: containerSize.height - layout.mainColumn.insets.top)) self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height)) self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) @@ -591,20 +608,20 @@ final class VideoChatParticipantsComponent: Component { } final class View: UIView, UIScrollViewDelegate { - private var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? - private let scrollViewClippingContainer: SolidRoundedCornersContainer private let scrollView: ScrollView + private let scrollViewBottomShadowView: UIImageView private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer private let separateVideoScrollView: ScrollView - private var component: VideoChatParticipantsComponent? + private(set) var component: VideoChatParticipantsComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var ignoreScrolling: Bool = false + //TODO:release private var gridParticipants: [VideoParticipant] = [] private var listParticipants: [GroupCallParticipantsContext.Participant] = [] @@ -617,6 +634,7 @@ final class VideoChatParticipantsComponent: Component { private let expandedGridItemContainer: UIView private var expandedControlsView: ComponentView? private var expandedThumbnailsView: ComponentView? + private var expandedSpeakingToast: ComponentView? private var listItemViews: [EnginePeer.Id: ListItem] = [:] private let listItemViewContainer: UIView @@ -628,9 +646,19 @@ final class VideoChatParticipantsComponent: Component { private var appliedGridIsEmpty: Bool = true + private var isPinchToZoomActive: Bool = false + + private var stopRequestingNonCentralVideo: Bool = false + private var stopRequestingNonCentralVideoTimer: Foundation.Timer? + private var currentLoadMoreToken: String? + + private var mainScrollViewEventCycleState: EventCycleState? + private var separateVideoScrollViewEventCycleState: EventCycleState? + override init(frame: CGRect) { self.scrollViewClippingContainer = SolidRoundedCornersContainer() self.scrollView = ScrollView() + self.scrollViewBottomShadowView = UIImageView() self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer() self.separateVideoScrollView = ScrollView() @@ -680,6 +708,7 @@ final class VideoChatParticipantsComponent: Component { self.scrollViewClippingContainer.addSubview(self.scrollView) self.addSubview(self.scrollViewClippingContainer) self.addSubview(self.scrollViewClippingContainer.cornersView) + self.addSubview(self.scrollViewBottomShadowView) self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView) self.addSubview(self.separateVideoScrollViewClippingContainer) @@ -695,6 +724,10 @@ final class VideoChatParticipantsComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.stopRequestingNonCentralVideoTimer?.invalidate() + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let component = self.component else { return nil @@ -703,9 +736,19 @@ final class VideoChatParticipantsComponent: Component { if component.expandedVideoState != nil { if let result = self.expandedGridItemContainer.hitTest(self.convert(point, to: self.expandedGridItemContainer), with: event) { return result - } else { - return self } + + if component.layout.videoColumn != nil { + if let result = self.scrollViewClippingContainer.hitTest(self.convert(point, to: self.scrollViewClippingContainer), with: event) { + return result + } + } + + if !self.expandedGridItemContainer.bounds.contains(self.convert(point, to: self.expandedGridItemContainer)) && !self.scrollViewClippingContainer.bounds.contains(self.convert(point, to: self.scrollViewClippingContainer)) { + return nil + } + + return self } else { if let result = self.scrollViewClippingContainer.hitTest(self.convert(point, to: self.scrollViewClippingContainer), with: event) { return result @@ -737,7 +780,7 @@ final class VideoChatParticipantsComponent: Component { let velocity = recognizer.velocity(in: self) if abs(velocity.y) > 100.0 || abs(fraction) >= 0.5 { - component.updateMainParticipant(nil) + component.updateMainParticipant(nil, nil) } else { self.state?.updated(transition: .spring(duration: 0.4)) } @@ -748,10 +791,46 @@ final class VideoChatParticipantsComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { + if scrollView == self.scrollView { + if let eventCycleState = self.mainScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + self.ignoreScrolling = true + scrollView.contentOffset = CGPoint() + self.ignoreScrolling = false + return + } + } + } else if scrollView == self.separateVideoScrollView { + if let eventCycleState = self.separateVideoScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + self.ignoreScrolling = true + scrollView.contentOffset = CGPoint() + self.ignoreScrolling = false + return + } + } + } + self.updateScrolling(transition: .immediate) } } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if scrollView == self.scrollView { + if let eventCycleState = self.mainScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + targetContentOffset.pointee.y = 0.0 + } + } + } else if scrollView == self.separateVideoScrollView { + if let eventCycleState = self.separateVideoScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + targetContentOffset.pointee.y = 0.0 + } + } + } + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -815,11 +894,18 @@ final class VideoChatParticipantsComponent: Component { var validGridItemIds: [VideoParticipantKey] = [] var validGridItemIndices: [Int] = [] + var clippedScrollViewBounds = self.scrollView.bounds + clippedScrollViewBounds.origin.y += component.layout.mainColumn.insets.top + clippedScrollViewBounds.size.height -= component.layout.mainColumn.insets.top + component.layout.mainColumn.insets.bottom + let visibleGridItemRange: (minIndex: Int, maxIndex: Int) + let clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int) if itemLayout.layout.videoColumn == nil { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) + clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds) } else { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) + clippedVisibleGridItemRange = visibleGridItemRange } if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex { @@ -835,6 +921,8 @@ final class VideoChatParticipantsComponent: Component { validGridItemIndices.append(index) } } + + var visibleParticipants: [EnginePeer.Id] = [] for index in validGridItemIndices { let videoParticipant = self.gridParticipants[index] @@ -862,6 +950,10 @@ final class VideoChatParticipantsComponent: Component { } } + if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) { + visibleParticipants.append(videoParticipant.key.id) + } + var suppressItemExpansionCollapseAnimation = false if isItemExpanded { if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey { @@ -885,6 +977,14 @@ final class VideoChatParticipantsComponent: Component { itemFrame = itemLayout.gridItemFrame(at: index) } + let itemReferenceX: CGFloat = itemFrame.minX + let itemContainerWidth: CGFloat + if isItemExpanded { + itemContainerWidth = expandedGridItemContainerFrame.width + } else { + itemContainerWidth = itemLayout.grid.containerSize.width + } + let itemContentInsets: UIEdgeInsets if isItemExpanded { itemContentInsets = itemLayout.expandedGrid.itemContainerInsets() @@ -895,7 +995,10 @@ final class VideoChatParticipantsComponent: Component { var itemControlInsets: UIEdgeInsets if isItemExpanded { itemControlInsets = itemContentInsets - itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0) + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden { + } else { + itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0) + } } else { itemControlInsets = itemContentInsets } @@ -912,31 +1015,66 @@ final class VideoChatParticipantsComponent: Component { let _ = itemView.view.update( transition: itemTransition, component: AnyComponent(VideoChatParticipantVideoComponent( + theme: component.theme, + strings: component.strings, call: component.call, participant: videoParticipant.participant, + isMyPeer: videoParticipant.participant.peer.id == component.participants?.myPeerId, isPresentation: videoParticipant.isPresentation, isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isExpanded: isItemExpanded, - isUIHidden: isItemUIHidden, + isUIHidden: isItemUIHidden || self.isPinchToZoomActive, contentInsets: itemContentInsets, controlInsets: itemControlInsets, interfaceOrientation: component.interfaceOrientation, - rootVideoLoadingEffectView: self.rootVideoLoadingEffectView, action: { [weak self] in guard let self, let component = self.component else { return } - if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { - component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden) + + if self.gridParticipants.count == 1, component.layout.videoColumn != nil { + if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { + component.updateMainParticipant(nil, false) + } else { + component.updateMainParticipant(videoParticipantKey, true) + } } else { - component.updateMainParticipant(videoParticipantKey) + if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { + component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden) + } else { + component.updateMainParticipant(videoParticipantKey, nil) + } } - } + }, + contextAction: !isItemExpanded ? { [weak self] peer, sourceView, gesture in + guard let self, let component = self.component else { + return + } + component.openParticipantContextMenu(peer.id, sourceView, gesture) + } : nil, + activatePinch: isItemExpanded ? { [weak self] sourceNode in + guard let self, let component = self.component else { + return + } + self.isPinchToZoomActive = true + self.state?.updated(transition: .immediate, isLocal: true) + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + component.call.accountContext.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } : nil, + deactivatedPinch: isItemExpanded ? { [weak self] in + guard let self else { + return + } + self.isPinchToZoomActive = false + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } : nil )), environment: {}, containerSize: itemFrame.size ) - if let itemComponentView = itemView.view.view { + if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View { if itemComponentView.superview == nil { itemComponentView.layer.allowsGroupOpacity = true @@ -952,6 +1090,7 @@ final class VideoChatParticipantsComponent: Component { itemComponentView.frame = itemFrame itemComponentView.alpha = itemAlpha + itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemContainerWidth, positionX: itemReferenceX, transition: .immediate) if !resultingItemTransition.animation.isImmediate { resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) @@ -986,11 +1125,13 @@ final class VideoChatParticipantsComponent: Component { itemComponentView.center = targetLocalItemFrame.center itemComponentView.bounds = CGRect(origin: CGPoint(), size: targetLocalItemFrame.size) }) + itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemLayout.containerSize.width, positionX: itemFrame.minX, transition: commonGridItemTransition) } } if !itemView.isCollapsing { resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center) resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemLayout.containerSize.width, positionX: itemFrame.minX, transition: resultingItemTransition) let resultingItemAlphaTransition: ComponentTransition if !resultingItemTransition.animation.isImmediate { @@ -1028,11 +1169,16 @@ final class VideoChatParticipantsComponent: Component { var validListItemIds: [EnginePeer.Id] = [] let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds) + let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds) if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex { for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex { let participant = self.listParticipants[i] validListItemIds.append(participant.peer.id) + if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex { + visibleParticipants.append(participant.peer.id) + } + var itemTransition = transition let itemView: ListItem if let current = self.listItemViews[participant.peer.id] { @@ -1047,11 +1193,17 @@ final class VideoChatParticipantsComponent: Component { let subtitle: PeerListItemComponent.Subtitle if participant.peer.id == component.call.accountContext.account.peerId { - subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent) } else if component.speakingParticipants.contains(participant.peer.id) { - subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive) + if let volume = participant.volume, volume / 100 != 100 { + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeakingVolume("\(volume / 100)%").string, color: .constructive) + } else { + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeaking, color: .constructive) + } + } else if let about = participant.about, !about.isEmpty { + subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral) } else { - subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral) } let rightAccessoryComponent: AnyComponent = AnyComponent(VideoChatParticipantStatusComponent( @@ -1073,6 +1225,7 @@ final class VideoChatParticipantsComponent: Component { avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent( call: component.call, peer: EnginePeer(participant.peer), + myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId, isSpeaking: component.speakingParticipants.contains(participant.peer.id), theme: component.theme )), @@ -1206,17 +1359,8 @@ final class VideoChatParticipantsComponent: Component { )) }*/ - let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0 + let expandedControlsAlpha: CGFloat = (expandedVideoState.isUIHidden || self.isPinchToZoomActive) ? 0.0 : 1.0 let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha - /*if itemLayout.layout.videoColumn == nil { - if expandedVideoState.isUIHidden { - expandedThumbnailsAlpha = 0.0 - } else { - expandedThumbnailsAlpha = 1.0 - } - } else { - expandedThumbnailsAlpha = 0.0 - }*/ var expandedThumbnailsTransition = transition let expandedThumbnailsView: ComponentView @@ -1237,17 +1381,27 @@ final class VideoChatParticipantsComponent: Component { return VideoChatExpandedParticipantThumbnailsComponent.Participant.Key(id: expandedVideoState.mainParticipant.id, isPresentation: expandedVideoState.mainParticipant.isPresentation) }, speakingParticipants: component.speakingParticipants, + interfaceOrientation: component.interfaceOrientation, updateSelectedParticipant: { [weak self] key in guard let self, let component = self.component else { return } - component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation)) + component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil) + }, + contextAction: { [weak self] peer, sourceView, gesture in + guard let self, let component = self.component else { + return + } + component.openParticipantContextMenu(peer.id, sourceView, gesture) } )), environment: {}, containerSize: itemLayout.expandedGrid.itemContainerFrame().size ) - let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) + var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) + if expandedVideoState.isUIHidden { + expandedThumbnailsFrame.origin.y += expandedThumbnailsSize.height + } if let expandedThumbnailsComponentView = expandedThumbnailsView.view { if expandedThumbnailsComponentView.superview == nil { self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView) @@ -1289,7 +1443,7 @@ final class VideoChatParticipantsComponent: Component { guard let self, let component = self.component else { return } - component.updateMainParticipant(nil) + component.updateMainParticipant(nil, nil) }, pinAction: { [weak self] in guard let self, let component = self.component else { @@ -1371,6 +1525,87 @@ final class VideoChatParticipantsComponent: Component { } } } + + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { $0 != expandedVideoState.mainParticipant.id }), let speakingPeer = participants.participants.first(where: { $0.peer.id == firstOther })?.peer { + let expandedSpeakingToast: ComponentView + var expandedSpeakingToastTransition = transition + if let current = self.expandedSpeakingToast { + expandedSpeakingToast = current + } else { + expandedSpeakingToastTransition = expandedSpeakingToastTransition.withAnimation(.none) + expandedSpeakingToast = ComponentView() + self.expandedSpeakingToast = expandedSpeakingToast + } + let expandedSpeakingToastSize = expandedSpeakingToast.update( + transition: expandedSpeakingToastTransition, + component: AnyComponent(VideoChatExpandedSpeakingToastComponent( + context: component.call.accountContext, + peer: EnginePeer(speakingPeer), + strings: component.strings, + theme: component.theme, + action: { [weak self] peer in + guard let self, let component = self.component, let participants = component.participants else { + return + } + guard let participant = participants.participants.first(where: { $0.peer.id == peer.id }) else { + return + } + var key: VideoParticipantKey? + if participant.presentationDescription != nil { + key = VideoParticipantKey(id: peer.id, isPresentation: true) + } else if participant.videoDescription != nil { + key = VideoParticipantKey(id: peer.id, isPresentation: false) + } + if let key { + component.updateMainParticipant(key, nil) + } + } + )), + environment: {}, + containerSize: itemLayout.expandedGrid.itemContainerFrame().size + ) + let expandedSpeakingToastFrame = CGRect(origin: CGPoint(x: floor((itemLayout.expandedGrid.itemContainerFrame().size.width - expandedSpeakingToastSize.width) * 0.5), y: 44.0), size: expandedSpeakingToastSize) + if let expandedSpeakingToastView = expandedSpeakingToast.view { + var animateIn = false + if expandedSpeakingToastView.superview == nil { + animateIn = true + self.expandedGridItemContainer.addSubview(expandedSpeakingToastView) + } + expandedSpeakingToastTransition.setFrame(view: expandedSpeakingToastView, frame: expandedSpeakingToastFrame) + + if animateIn { + alphaTransition.animateAlpha(view: expandedSpeakingToastView, from: 0.0, to: 1.0) + transition.animateScale(view: expandedSpeakingToastView, from: 0.6, to: 1.0) + } + } + } else { + if let expandedSpeakingToast = self.expandedSpeakingToast { + self.expandedSpeakingToast = nil + if let expandedSpeakingToastView = expandedSpeakingToast.view { + alphaTransition.setAlpha(view: expandedSpeakingToastView, alpha: 0.0, completion: { [weak expandedSpeakingToastView] _ in + expandedSpeakingToastView?.removeFromSuperview() + }) + transition.setScale(view: expandedSpeakingToastView, scale: 0.6) + } + } + } + + if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 { + if self.currentLoadMoreToken != loadMoreToken { + self.currentLoadMoreToken = loadMoreToken + component.call.loadMoreMembers(token: loadMoreToken) + } + } + + component.visibleParticipantsUpdated(Set(visibleParticipants)) + } + + func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) { + if scrollView == self.scrollView { + self.mainScrollViewEventCycleState = eventCycleState + } else if scrollView == self.separateVideoScrollView { + self.separateVideoScrollViewEventCycleState = eventCycleState + } } func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -1379,9 +1614,33 @@ final class VideoChatParticipantsComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden { + if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState { + self.stopRequestingNonCentralVideoTimer?.invalidate() + + self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.stopRequestingNonCentralVideo = true + self.stopRequestingNonCentralVideoTimer = nil + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + }) + } + } else { + self.stopRequestingNonCentralVideo = false + if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer { + self.stopRequestingNonCentralVideoTimer = nil + stopRequestingNonCentralVideoTimer.invalidate() + } + } + let measureListItemSize = self.measureListItemView.update( transition: .immediate, component: AnyComponent(PeerListItemComponent( @@ -1408,12 +1667,12 @@ final class VideoChatParticipantsComponent: Component { if let participants = component.participants, let inviteType = participants.inviteType { switch inviteType { case .invite: - inviteText = "Invite Members" + inviteText = component.strings.VoiceChat_InviteMember case .shareLink: - inviteText = "Share Invite Link" + inviteText = component.strings.VoiceChat_Share } } else { - inviteText = "Invite Members" + inviteText = component.strings.VoiceChat_InviteMember } let inviteListItemSize = self.inviteListItemView.update( transition: transition, @@ -1435,11 +1694,16 @@ final class VideoChatParticipantsComponent: Component { var listParticipants: [GroupCallParticipantsContext.Participant] = [] if let participants = component.participants { for participant in participants.participants { + var isFullyMuted = false + if let muteState = participant.muteState, !muteState.canUnmute { + isFullyMuted = true + } + var hasVideo = false if participant.videoDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: false) - if participant.peer.id == component.call.accountContext.account.peerId || participant.peer.id == participants.myPeerId { + if participant.peer.id == participants.myPeerId { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) @@ -1448,14 +1712,14 @@ final class VideoChatParticipantsComponent: Component { if participant.presentationDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: true) - if participant.peer.id == component.call.accountContext.account.peerId { + if participant.peer.id == participants.myPeerId { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) } } if !hasVideo || component.layout.videoColumn != nil { - if participant.peer.id == component.call.accountContext.account.peerId { + if participant.peer.id == participants.myPeerId && !isFullyMuted { listParticipants.insert(participant, at: 0) } else { listParticipants.append(participant) @@ -1524,13 +1788,19 @@ final class VideoChatParticipantsComponent: Component { } if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) { - if !requestedVideo.contains(videoChannel) { - requestedVideo.append(videoChannel) + if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full { + } else { + if !requestedVideo.contains(videoChannel) { + requestedVideo.append(videoChannel) + } } } if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) { - if !requestedVideo.contains(videoChannel) { - requestedVideo.append(videoChannel) + if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full { + } else { + if !requestedVideo.contains(videoChannel) { + requestedVideo.append(videoChannel) + } } } } @@ -1547,6 +1817,37 @@ final class VideoChatParticipantsComponent: Component { smoothCorners: false ), transition: transition) + if self.scrollViewBottomShadowView.image == nil { + let height: CGFloat = 80.0 + let baseGradientAlpha: CGFloat = 1.0 + let numSteps = 8 + let firstStep = 0 + let firstLocation = 0.0 + let colors = (0 ..< numSteps).map { i -> UIColor in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 0.0, alpha: baseGradientAlpha * value) + } + } + let locations = (0 ..< numSteps).map { i -> CGFloat in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + self.scrollViewBottomShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0)) + self.scrollViewBottomShadowView.tintColor = .black + } + let scrollViewBottomShadowOverflow: CGFloat = 30.0 + let scrollViewBottomShadowFrame = CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX, y: itemLayout.scrollClippingFrame.maxY - component.layout.mainColumn.insets.bottom - scrollViewBottomShadowOverflow), size: CGSize(width: itemLayout.scrollClippingFrame.width, height: component.layout.mainColumn.insets.bottom + scrollViewBottomShadowOverflow)) + transition.setFrame(view: self.scrollViewBottomShadowView, frame: scrollViewBottomShadowFrame) + transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center) transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size)) transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift new file mode 100644 index 00000000000..c9ae7d1dea6 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift @@ -0,0 +1,213 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import TelegramStringFormatting +import HierarchyTrackingLayer + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +private let latePurple = UIColor(rgb: 0x974aa9) +private let latePink = UIColor(rgb: 0xf0436c) + +final class VideoChatScheduledInfoComponent: Component { + let timestamp: Int32 + let strings: PresentationStrings + + init( + timestamp: Int32, + strings: PresentationStrings + ) { + self.timestamp = timestamp + self.strings = strings + } + + static func ==(lhs: VideoChatScheduledInfoComponent, rhs: VideoChatScheduledInfoComponent) -> Bool { + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let countdownText = ComponentView() + private let dateText = ComponentView() + + private let countdownContainerView: UIView + private let countdownMaskView: UIView + private let countdownGradientLayer: SimpleGradientLayer + private let hierarchyTrackingLayer: HierarchyTrackingLayer + + private var component: VideoChatScheduledInfoComponent? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + self.countdownContainerView = UIView() + self.countdownMaskView = UIView() + + self.countdownGradientLayer = SimpleGradientLayer() + self.countdownGradientLayer.type = .radial + self.countdownGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.countdownGradientLayer.locations = [0.0, 0.85, 1.0] + self.countdownGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.countdownGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + + self.countdownContainerView.layer.addSublayer(self.countdownGradientLayer) + self.addSubview(self.countdownContainerView) + + self.countdownContainerView.mask = self.countdownMaskView + + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + if value { + self.updateAnimations() + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateAnimations() { + if let _ = self.countdownGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.countdownGradientLayer.startPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.countdownGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + guard let self else { + return + } + if self.hierarchyTrackingLayer.isInHierarchy { + self.updateAnimations() + } + } + + self.countdownGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0) + ) + + let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970)) + let countdownText: String + if remainingSeconds >= 86400 { + countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + } else { + countdownText = textForTimeout(value: abs(remainingSeconds)) + /*if remainingSeconds < 0 && !self.isLate { + self.isLate = true + self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] + }*/ + } + + let countdownTextSize = self.countdownText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) + ) + + let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: component.timestamp, alwaysShowTime: true).string + + let dateTextSize = self.dateText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: dateText, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) + ) + + let titleSpacing: CGFloat = 5.0 + let dateSpacing: CGFloat = 5.0 + + let contentHeight: CGFloat = titleSize.height + titleSpacing + countdownTextSize.height + dateSpacing + dateTextSize.height + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) + let countdownTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - countdownTextSize.width) * 0.5), y: titleFrame.maxY + titleSpacing), size: countdownTextSize) + let dateTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dateTextSize.width) * 0.5), y: countdownTextFrame.maxY + dateSpacing), size: dateTextSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + if let countdownTextView = self.countdownText.view { + if countdownTextView.superview == nil { + self.countdownMaskView.addSubview(countdownTextView) + } + transition.setFrame(view: countdownTextView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + } + + transition.setFrame(view: self.countdownContainerView, frame: countdownTextFrame) + transition.setFrame(view: self.countdownMaskView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + transition.setFrame(layer: self.countdownGradientLayer, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + + if let dateTextView = self.dateText.view { + if dateTextView.superview == nil { + self.addSubview(dateTextView) + } + transition.setPosition(view: dateTextView, position: dateTextFrame.center) + dateTextView.bounds = CGRect(origin: CGPoint(), size: dateTextFrame.size) + } + + self.updateAnimations() + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index da973d873d6..751c4db8b95 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -21,18 +21,10 @@ import UndoUI import ShareController import AvatarNode import TelegramAudio - -import PeerInfoUI - -import DeleteChatPeerActionSheetItem -import PeerListItemComponent import LegacyComponents -import LegacyUI -import WebSearchUI -import MapResourceToAvatarSizes -import LegacyMediaPickerUI +import TooltipUI -private final class VideoChatScreenComponent: Component { +final class VideoChatScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let initialData: VideoChatScreenV2Impl.InitialData @@ -50,68 +42,82 @@ private final class VideoChatScreenComponent: Component { return true } - private struct PanGestureState { - var offsetFraction: CGFloat + private final class PanState { + var fraction: CGFloat + weak var scrollView: UIScrollView? + var startContentOffsetY: CGFloat = 0.0 + var accumulatedOffset: CGFloat = 0.0 + var dismissedTooltips: Bool = false + var didLockScrolling: Bool = false + var contentOffset: CGFloat? - init(offsetFraction: CGFloat) { - self.offsetFraction = offsetFraction + init(fraction: CGFloat, scrollView: UIScrollView?) { + self.fraction = fraction + self.scrollView = scrollView } } - final class View: UIView { - private let containerView: UIView + final class View: UIView, UIGestureRecognizerDelegate { + let containerView: UIView - private var component: VideoChatScreenComponent? - private var environment: ViewControllerComponentContainer.Environment? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false + var component: VideoChatScreenComponent? + var environment: ViewControllerComponentContainer.Environment? + weak var state: EmptyComponentState? + var isUpdating: Bool = false - private var panGestureState: PanGestureState? - private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false - private var completionOnPanGestureApply: (() -> Void)? + private var verticalPanState: PanState? + var notifyDismissedInteractivelyOnPanGestureApply: Bool = false + var completionOnPanGestureApply: (() -> Void)? - private let videoRenderingContext = VideoRenderingContext() + let videoRenderingContext = VideoRenderingContext() - private let title = ComponentView() - private let navigationLeftButton = ComponentView() - private let navigationRightButton = ComponentView() - private var navigationSidebarButton: ComponentView? + let title = ComponentView() + let navigationLeftButton = ComponentView() + let navigationRightButton = ComponentView() + var navigationSidebarButton: ComponentView? - private let videoButton = ComponentView() - private let leaveButton = ComponentView() - private let microphoneButton = ComponentView() + let videoButton = ComponentView() + let leaveButton = ComponentView() + let microphoneButton = ComponentView() - private let participants = ComponentView() + let participants = ComponentView() + var scheduleInfo: ComponentView? - private var reconnectedAsEventsDisposable: Disposable? + var reconnectedAsEventsDisposable: Disposable? + var memberEventsDisposable: Disposable? - private var peer: EnginePeer? - private var callState: PresentationGroupCallState? - private var stateDisposable: Disposable? + var peer: EnginePeer? + var callState: PresentationGroupCallState? + var stateDisposable: Disposable? - private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? - private var audioOutputStateDisposable: Disposable? + var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? + var audioOutputStateDisposable: Disposable? - private var displayAsPeers: [FoundPeer]? - private var displayAsPeersDisposable: Disposable? + var displayAsPeers: [FoundPeer]? + var displayAsPeersDisposable: Disposable? - private var inviteLinks: GroupCallInviteLinks? - private var inviteLinksDisposable: Disposable? + var inviteLinks: GroupCallInviteLinks? + var inviteLinksDisposable: Disposable? - private var isPushToTalkActive: Bool = false + var isPushToTalkActive: Bool = false - private var members: PresentationGroupCallMembers? - private var membersDisposable: Disposable? + var members: PresentationGroupCallMembers? + var membersDisposable: Disposable? - private let isPresentedValue = ValuePromise(false, ignoreRepeated: true) - private var applicationStateDisposable: Disposable? + var speakingParticipantPeers: [EnginePeer] = [] + var visibleParticipants: Set = Set() - private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? + let isPresentedValue = ValuePromise(false, ignoreRepeated: true) + var applicationStateDisposable: Disposable? - private let inviteDisposable = MetaDisposable() - private let currentAvatarMixin = Atomic(value: nil) - private let updateAvatarDisposable = MetaDisposable() - private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? + var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? + var focusedSpeakerAutoSwitchDeadline: Double = 0.0 + var isTwoColumnSidebarHidden: Bool = false + + let inviteDisposable = MetaDisposable() + let currentAvatarMixin = Atomic(value: nil) + let updateAvatarDisposable = MetaDisposable() + var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? override init(frame: CGRect) { self.containerView = UIView() @@ -124,9 +130,11 @@ private final class VideoChatScreenComponent: Component { self.addSubview(self.containerView) - self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) } required init?(coder: NSCoder) { @@ -138,6 +146,7 @@ private final class VideoChatScreenComponent: Component { self.membersDisposable?.dispose() self.applicationStateDisposable?.dispose() self.reconnectedAsEventsDisposable?.dispose() + self.memberEventsDisposable?.dispose() self.displayAsPeersDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.inviteLinksDisposable?.dispose() @@ -146,1622 +155,238 @@ private final class VideoChatScreenComponent: Component { } func animateIn() { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.state?.updated(transition: .immediate) - self.panGestureState = nil + self.verticalPanState = nil self.state?.updated(transition: .spring(duration: 0.5)) } func animateOut(completion: @escaping () -> Void) { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.completionOnPanGestureApply = completion self.state?.updated(transition: .spring(duration: 0.5)) } - @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began, .changed: - if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply { - let translation = recognizer.translation(in: self) - self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) - self.state?.updated(transition: .immediate) - } - case .cancelled, .ended: - if !self.bounds.height.isZero { - let translation = recognizer.translation(in: self) - let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) - - let velocity = recognizer.velocity(in: self) - - self.panGestureState = nil - if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { - self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) - self.notifyDismissedInteractivelyOnPanGestureApply = true - if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { - controller.notifyDismissed() - } - } - - self.state?.updated(transition: .spring(duration: 0.4)) + @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UITapGestureRecognizer { + if otherGestureRecognizer is UIPanGestureRecognizer { + return true } - default: - break + return false + } else { + return false } } - private func openMoreMenu() { - guard let sourceView = self.navigationLeftButton.view else { - return - } - guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { - return - } - guard let peer = self.peer else { - return - } - guard let callState = self.callState else { - return - } - - let canManageCall = callState.canManageCall - - var items: [ContextMenuItem] = [] - - if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { - for peer in displayAsPeers { - if peer.peer.id == callState.myPeerId { - let avatarSize = CGSize(width: 28.0, height: 28.0) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuDisplayAsItems())))) - }))) - items.append(.separator) - break + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer { + if let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { + if otherGestureRecognizer.view is UIScrollView { + return true } - } - } - - if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { - var currentOutputTitle = "" - for output in availableOutputs { - if output == currentOutput { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = environment.strings.Call_AudioRouteSpeaker - case .headphones: - title = environment.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name + if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + if otherGestureRecognizer.view === participantsView { + return true } - currentOutputTitle = title - break - } - } - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuAudioItems())))) - }))) - } - - if canManageCall { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_EditTitle - } else { - text = environment.strings.VoiceChat_EditTitle - } - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - self.openTitleEditing() - }))) - - var hasPermissions = true - if case let .channel(chatPeer) = peer { - if case .broadcast = chatPeer.info { - hasPermissions = false - } else if chatPeer.flags.contains(.isGigagroup) { - hasPermissions = false } } - if hasPermissions { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuPermissionItems())))) - }))) - } - } - - if let inviteLinks = self.inviteLinks { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_Share, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - self.presentShare(inviteLinks) - }))) - } - - //let isScheduled = strongSelf.isScheduled - //TODO:release - let isScheduled: Bool = !"".isEmpty - - let canSpeak: Bool - if let muteState = callState.muteState { - canSpeak = muteState.canUnmute + return false } else { - canSpeak = true - } - - if !isScheduled && canSpeak { - if #available(iOS 15.0, *) { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - AVCaptureDevice.showSystemUserInterface(.microphoneModes) - }))) - } - } - - if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { - if component.call.hasScreencast { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self, let component = self.component else { - return - } - component.call.disableScreencast() - }))) - } else { - items.append(.custom(VoiceChatShareScreenContextItem(context: component.call.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { _, _ in }), false)) - } + return false } - - if canManageCall { - if let recordingStartTimestamp = callState.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - component.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) - - Queue.mainQueue().after(0.88) { - HapticFeedback().success() - } - - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_RecordingSaved - } else { - text = environment.strings.VideoChat_RecordingSaved - } - self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in - if case .info = value, let self, let component = self.component, let environment = self.environment, let navigationController = environment.controller()?.navigationController as? NavigationController { - let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().justDispatch { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let peer, let navigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - }) - } - }) - - return true - } - return false - }) - })]) - environment.controller()?.present(alertController, in: .window(.root)) - }), false)) - } else { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_StartRecording + } + + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply { + let translation = recognizer.translation(in: self) + let fraction = max(0.0, translation.y / self.bounds.height) + if let verticalPanState = self.verticalPanState { + verticalPanState.fraction = fraction } else { - text = environment.strings.VoiceChat_StartRecording - } - if callState.scheduleTimestamp == nil { - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { - return - } - - let controller = VoiceChatRecordingSetupController(context: component.call.accountContext, peer: peer, completion: { [weak self] videoOrientation in - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { - return - } - let title: String - let text: String - let placeholder: String - if let _ = videoOrientation { - placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholderVideo - } else { - placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholder - } - if case let .channel(channel) = peer, case .broadcast = channel.info { - title = environment.strings.LiveStream_StartRecordingTitle - if let _ = videoOrientation { - text = environment.strings.LiveStream_StartRecordingTextVideo - } else { - text = environment.strings.LiveStream_StartRecordingText - } - } else { - title = environment.strings.VoiceChat_StartRecordingTitle - if let _ = videoOrientation { - text = environment.strings.VoiceChat_StartRecordingTextVideo - } else { - text = environment.strings.VoiceChat_StartRecordingText + var targetScrollView: UIScrollView? + if case .began = recognizer.state, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + if let hitResult = participantsView.hitTest(self.convert(recognizer.location(in: self), to: participantsView), with: nil) { + func findTargetScrollView(target: UIView, minParent: UIView) -> UIScrollView? { + if target === participantsView { + return nil } - } - - let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer, let title else { - return + if let target = target as? UIScrollView { + return target } - - component.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) - - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_RecordingStarted + if let parent = target.superview { + return findTargetScrollView(target: parent, minParent: minParent) } else { - text = environment.strings.VoiceChat_RecordingStarted + return nil } - - self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) - component.call.playTone(.recordingStarted) - }) - environment.controller()?.present(controller, in: .window(.root)) - }) - environment.controller()?.present(controller, in: .window(.root)) - }))) - } - } - } - - if canManageCall { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream - } else { - text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat - } - items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let action: () -> Void = { [weak self] in - guard let self, let component = self.component else { - return - } - - let _ = (component.call.leave(terminateIfPossible: true) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let environment = self.environment else { - return + } + targetScrollView = findTargetScrollView(target: hitResult, minParent: participantsView) } - environment.controller()?.dismiss() - }) - } - - let title: String - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle - text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText - } else { - title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle - text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText - } - - let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { - action() - })]) - environment.controller()?.present(alertController, in: .window(.root)) - }))) - } else { - let leaveText: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - leaveText = environment.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = environment.strings.VoiceChat_LeaveVoiceChat - } - items.append(.action(ContextMenuActionItem(text: leaveText, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - - let _ = (component.call.leave(terminateIfPossible: false) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let environment = self.environment else { - return - } - environment.controller()?.dismiss() - }) - }))) - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) - controller.presentInGlobalOverlay(contextController) - } - - private func contextMenuDisplayAsItems() -> [ContextMenuItem] { - guard let component = self.component, let environment = self.environment else { - return [] - } - guard let callState = self.callState else { - return [] - } - let myPeerId = callState.myPeerId - - let avatarSize = CGSize(width: 28.0, height: 28.0) - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - var isGroup = false - if let displayAsPeers = self.displayAsPeers { - for peer in displayAsPeers { - if peer.peer is TelegramGroup { - isGroup = true - break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { - isGroup = true - break - } - } - } - - items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? environment.strings.VoiceChat_DisplayAsInfoGroup : environment.strings.VoiceChat_DisplayAsInfo, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) - }), true)) - - if let displayAsPeers = self.displayAsPeers { - for peer in displayAsPeers { - var subtitle: String? - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - subtitle = environment.strings.VoiceChat_PersonalAccount - } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { - subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) - } else { - subtitle = environment.strings.Conversation_StatusMembers(subscribers) } - } - - let isSelected = peer.peer.id == myPeerId - let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) - let theme = environment.theme - let avatarSignal = peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) - |> map { image -> UIImage? in - if isSelected, let image = image { - return generateImage(extendedAvatarSize, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) - - let lineWidth = 1.0 + UIScreenPixel - context.setLineWidth(lineWidth) - context.setStrokeColor(theme.actionSheet.controlAccentColor.cgColor) - context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) - }) - } else { - return image + self.verticalPanState = PanState(fraction: fraction, scrollView: targetScrollView) + if let targetScrollView { + self.verticalPanState?.contentOffset = targetScrollView.contentOffset.y + self.verticalPanState?.startContentOffsetY = recognizer.translation(in: self).y } } - items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in - f(.default) + if let verticalPanState = self.verticalPanState { + /*if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips { + verticalPanState.dismissedTooltips = true + self.dismissAllTooltips() + }*/ - guard let self, let component = self.component else { - return + if let scrollView = verticalPanState.scrollView { + let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY + let overflowY = scrollView.contentOffset.y - relativeTranslationY + + if !verticalPanState.didLockScrolling { + if scrollView.contentOffset.y == 0.0 { + verticalPanState.didLockScrolling = true + } + if let previousContentOffset = verticalPanState.contentOffset, (previousContentOffset < 0.0) != (scrollView.contentOffset.y < 0.0) { + verticalPanState.didLockScrolling = true + } + } + + var resetContentOffset = false + if verticalPanState.didLockScrolling { + verticalPanState.accumulatedOffset += -overflowY + + if verticalPanState.accumulatedOffset < 0.0 { + verticalPanState.accumulatedOffset = 0.0 + } + if scrollView.contentOffset.y < 0.0 { + resetContentOffset = true + } + } else { + verticalPanState.accumulatedOffset += -overflowY + verticalPanState.accumulatedOffset = max(0.0, verticalPanState.accumulatedOffset) + } + + if verticalPanState.accumulatedOffset > 0.0 || resetContentOffset { + scrollView.contentOffset = CGPoint() + + if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + let eventCycleState = VideoChatParticipantsComponent.EventCycleState() + eventCycleState.ignoreScrolling = true + participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: eventCycleState) + + DispatchQueue.main.async { [weak scrollView, weak participantsView] in + guard let participantsView, let scrollView else { + return + } + participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: nil) + } + } + } + + verticalPanState.contentOffset = scrollView.contentOffset.y + verticalPanState.startContentOffsetY = recognizer.translation(in: self).y } - if peer.peer.id != myPeerId { - component.call.reconnect(as: peer.peer.id) - } - }))) - - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - items.append(.separator) + self.state?.updated(transition: .immediate) } } - } - return items - } - - private func contextMenuAudioItems() -> [ContextMenuItem] { - guard let environment = self.environment else { - return [] - } - guard let (availableOutputs, currentOutput) = self.audioOutputState else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - for output in availableOutputs { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = environment.strings.Call_AudioRouteSpeaker - case .headphones: - title = environment.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - } - items.append(.action(ContextMenuActionItem(text: title, icon: { theme in - if output == currentOutput { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + case .cancelled, .ended: + if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState { + let translation = recognizer.translation(in: self) + verticalPanState.fraction = max(0.0, translation.y / self.bounds.height) + + let effectiveFraction: CGFloat + if verticalPanState.scrollView != nil { + effectiveFraction = verticalPanState.accumulatedOffset / self.bounds.height } else { - return nil + effectiveFraction = verticalPanState.fraction } - }, action: { [weak self] _, f in - f(.default) - guard let self, let component = self.component else { - return + let velocity = recognizer.velocity(in: self) + + self.verticalPanState = nil + if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) { + self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil) + self.notifyDismissedInteractivelyOnPanGestureApply = true + if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { + controller.notifyDismissed() + } } - component.call.setCurrentAudioOutput(output) - }))) + self.state?.updated(transition: .spring(duration: 0.4)) + } + default: + break } - - return items } - private func contextMenuPermissionItems() -> [ContextMenuItem] { - guard let environment = self.environment, let callState = self.callState else { - return [] - } - - var items: [ContextMenuItem] = [] - if callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in - if isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - component.call.updateDefaultParticipantsAreMuted(isMuted: false) - }))) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in - if !isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - component.call.updateDefaultParticipantsAreMuted(isMuted: true) - }))) - } - return items - } - - private func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { - guard let component = self.component, let environment = self.environment else { - return - } - guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else { - return - } - - let muteStatePromise = Promise(participant.muteState) - - let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in - guard let self, let component = self.component, let environment = self.environment else { - return [] - } - guard let callState = self.callState else { - return [] - } - - var items: [ContextMenuItem] = [] - - var hasVolumeSlider = false - let peer = participant.peer - if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { - } else { - if callState.canManageCall || callState.myPeerId != id { - hasVolumeSlider = true - - let minValue: CGFloat - if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { - minValue = 0.01 - } else { - minValue = 0.0 - } - items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in - guard let self, let component = self.component else { - return - } - - if finished && newValue.isZero { - let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true) - muteStatePromise.set(.single(updatedMuteState)) - } else { - component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) - } - }), true)) - } - } - - if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) { - items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) - }), true)) - } - - if peer.id == callState.myPeerId { - if participant.hasRaiseHand { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - component.call.lowerHand() - - f(.default) - }))) - } - items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - Queue.mainQueue().after(0.1) { - guard let self else { - return - } - - self.openAvatarForEditing(fromGallery: false, completion: {}) - } - }))) - - items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - Queue.mainQueue().after(0.1) { - guard let self, let component = self.component, let environment = self.environment else { - return - } - let maxBioLength: Int - if peer.id.namespace == Namespaces.Peer.CloudUser { - maxBioLength = 70 - } else { - maxBioLength = 100 - } - let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in - guard let self, let component = self.component, let environment = self.environment, let bio else { - return - } - if peer.id.namespace == Namespaces.Peer.CloudUser { - let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } else { - let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } - - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) - }) - environment.controller()?.present(controller, in: .window(.root)) - } - }))) - - if let peer = peer as? TelegramUser { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - Queue.mainQueue().after(0.1) { - guard let self, let component = self.component, let environment = self.environment else { - return - } - let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in - guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else { - return - } - let _ = component.call.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() - - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) - }) - environment.controller()?.present(controller, in: .window(.root)) - } - }))) - } - } else { - if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) { - if callState.adminIds.contains(peer.id) { - if let _ = muteState { - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } else { - if let muteState = muteState, !muteState.canUnmute { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - - self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - } else { - if let muteState = muteState, muteState.mutedByYou { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - - let openTitle: String - let openIcon: UIImage? - if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - openTitle = environment.strings.VoiceChat_OpenChannel - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") - } else { - openTitle = environment.strings.VoiceChat_OpenGroup - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") - } - } else { - openTitle = environment.strings.Conversation_ContextMenuSendMessage - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") - } - items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in - return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { - return - } - - let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().after(0.3) { - guard let navigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - f(.dismissWithoutContent) - }))) - - if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self, let component = self.component else { - return - } - - let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - - let nameDisplayOrder = presentationData.nameDisplayOrder - items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) - - items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: component.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() - component.call.removedPeer(peer.id) - - self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - environment.controller()?.present(actionSheet, in: .window(.root)) - }) - }) - }))) - } - } - return items - } - - let items = muteStatePromise.get() - |> map { muteState -> [ContextMenuItem] in - return itemsForEntry(muteState) - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController( - presentationData: presentationData, - source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), - items: items |> map { items in - return ContextController.Items(content: .list(items)) - }, - recognizer: nil, - gesture: gesture - ) - - environment.controller()?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismiss() - } - return true - }) - - environment.controller()?.presentInGlobalOverlay(contextController) - } - - private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { + func openTitleEditing() { guard let component = self.component else { return - } - guard let callState = self.callState else { - return - } - let peerId = callState.myPeerId - - let _ = (component.call.accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in - guard let self, let component = self.component, let environment = self.environment else { - return - } - guard let peer else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - - let legacyController = LegacyController(presentation: .custom, theme: environment.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - self.endEditing(true) - environment.controller()?.present(legacyController, in: .window(.root)) - - var hasPhotos = false - if !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.forceDark = true - mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext) - let _ = self.currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { [weak self] assetsController in - guard let self, let component = self.component, let environment = self.environment else { - return - } - let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in - assetsController?.dismiss() - - guard let self else { - return - } - self.updateProfilePhoto(result) - })) - controller.navigationPresentation = .modal - environment.controller()?.push(controller) - - if fromGallery { - completion() - } - } - mixin.didFinishWithImage = { [weak self] image in - if let image = image { - completion() - self?.updateProfilePhoto(image) - } - } - mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in - if let image = image, let asset = asset { - completion() - self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) - } - } - mixin.didFinishWithDelete = { [weak self] in - guard let self, let environment = self.environment else { - return - } - - let proceed = { [weak self] in - guard let self, let component = self.component else { - return - } - - let _ = self.currentAvatarMixin.swap(nil) - let postbox = component.call.accountContext.account.postbox - self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - |> deliverOnMainQueue).start()) - } - - let actionSheet = ActionSheetController(presentationData: presentationData) - let items: [ActionSheetItem] = [ - ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - proceed() - }) - ] - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - environment.controller()?.present(actionSheet, in: .window(.root)) - } - mixin.didDismiss = { [weak self, weak legacyController] in - guard let self else { - return - } - let _ = self.currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - } - }) - } - - private func updateProfilePhoto(_ image: UIImage) { - guard let component = self.component else { - return - } - guard let callState = self.callState else { - return - } - guard let data = image.jpegData(compressionQuality: 0.6) else { - return - } - - let peerId = callState.myPeerId - - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - - self.currentUpdatingAvatar = (representation, 0.0) - - let postbox = component.call.account.postbox - let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - - self.updateAvatarDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { - return - } - switch result { - case .complete: - self.currentUpdatingAvatar = nil - self.state?.updated(transition: .spring(duration: 0.4)) - case let .progress(value): - self.currentUpdatingAvatar = (representation, value) - } - })) - - self.state?.updated(transition: .spring(duration: 0.4)) - } - - private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { - guard let component = self.component else { - return - } - guard let callState = self.callState else { - return - } - guard let data = image.jpegData(compressionQuality: 0.6) else { - return - } - let peerId = callState.myPeerId - - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - - self.currentUpdatingAvatar = (representation, 0.0) - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let context = component.call.accountContext - let account = context.account - let signal = Signal { [weak self] subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } else if let progress = next as? NSNumber { - Queue.mainQueue().async { [weak self] in - guard let self else { - return - } - self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25) - self.state?.updated(transition: .spring(duration: 0.4)) - } - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - self.updateAvatarDisposable.set((signal - |> mapToSignal { videoResource -> Signal in - if peerId.namespace == Namespaces.Peer.CloudUser { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } - } - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { - return - } - switch result { - case .complete: - self.currentUpdatingAvatar = nil - self.state?.updated(transition: .spring(duration: 0.4)) - case let .progress(value): - self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75) - self.state?.updated(transition: .spring(duration: 0.4)) - } - })) - } - - private func openTitleEditing() { - guard let component = self.component else { - return - } - - let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let self, let component = self.component, let environment = self.environment else { - return - } - guard let callState = self.callState, let peer = self.peer else { - return - } - - let initialTitle = callState.title - - let title: String - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - title = environment.strings.LiveStream_EditTitle - text = environment.strings.LiveStream_EditTitleText - } else { - title = environment.strings.VoiceChat_EditTitle - text = environment.strings.VoiceChat_EditTitleText - } - - let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in - guard let self, let component = self.component, let environment = self.environment else { - return - } - guard let title = title, title != initialTitle else { - return - } - - component.call.updateTitle(title) - - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = title.isEmpty ? environment.strings.LiveStream_EditTitleRemoveSuccess : environment.strings.LiveStream_EditTitleSuccess(title).string - } else { - text = title.isEmpty ? environment.strings.VoiceChat_EditTitleRemoveSuccess : environment.strings.VoiceChat_EditTitleSuccess(title).string - } - - self.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) - }) - environment.controller()?.present(controller, in: .window(.root)) - }) - } - - private func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) { - guard let component = self.component, let environment = self.environment else { - return - } - var animateInAsReplacement = false - environment.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - animateInAsReplacement = true - c.dismiss() - } - return true - } - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current) - } - - private func openInviteMembers() { - guard let component = self.component else { - return - } - - var canInvite = true - var inviteIsLink = false - if case let .channel(peer) = self.peer { - if peer.flags.contains(.isGigagroup) { - if peer.flags.contains(.isCreator) || peer.adminRights != nil { - } else { - canInvite = false - } - } - if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { - inviteIsLink = true - } - } - var inviteType: VideoChatParticipantsComponent.Participants.InviteType? - if canInvite { - if inviteIsLink { - inviteType = .shareLink - } else { - inviteType = .invite - } - } - - guard let inviteType else { - return - } - - switch inviteType { - case .invite: - let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId)) - let _ = (groupPeer - |> deliverOnMainQueue).start(next: { [weak self] groupPeer in - guard let self, let component = self.component, let environment = self.environment, let groupPeer else { - return - } - let inviteLinks = self.inviteLinks - - if case let .channel(groupPeer) = groupPeer { - var canInviteMembers = true - if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { - canInviteMembers = false - } - if !canInviteMembers { - if let inviteLinks { - self.presentShare(inviteLinks) - } - return - } - } - - var filters: [ChannelMembersSearchFilter] = [] - if let members = self.members { - filters.append(.disable(Array(members.participants.map { $0.peer.id }))) - } - if case let .channel(groupPeer) = groupPeer { - if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { - filters.append(.excludeNonMembers) - } - } else if case let .legacyGroup(groupPeer) = groupPeer { - if groupPeer.hasBannedPermission(.banAddMembers) { - filters.append(.excludeNonMembers) - } - } - filters.append(.excludeBots) - - var dismissController: (() -> Void)? - let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - guard let callState = self.callState else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - if peer.id == callState.myPeerId { - return - } - if let participant { - dismissController?() - - if component.call.invitePeer(participant.peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - } else { - if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { - let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in - dismissController?() - - guard let self, let component = self.component else { - return - } - - let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let self, let environment = self.environment else { - return - } - self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) - }) - })]), in: .window(.root)) - } else { - let text: String - if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { - text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } - - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - if case let .channel(groupPeer) = groupPeer { - guard let selfController = environment.controller() else { - return - } - let inviteDisposable = self.inviteDisposable - var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id]) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in - dismissController?() - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let text: String - switch error { - case .limitExceeded: - text = environment.strings.Channel_ErrorAddTooMuch - case .tooMuchJoined: - text = environment.strings.Invite_ChannelsTooMuch - case .generic: - text = environment.strings.Login_UnknownError - case .restricted: - text = environment.strings.Channel_ErrorAddBlocked - case .notMutualContact: - if case .broadcast = groupPeer.info { - text = environment.strings.Channel_AddUserLeftError - } else { - text = environment.strings.GroupInfo_AddUserLeftError - } - case .botDoesntSupportGroups: - text = environment.strings.Channel_BotDoesntSupportGroups - case .tooMuchBots: - text = environment.strings.Channel_TooMuchBots - case .bot: - text = environment.strings.Login_UnknownError - case .kicked: - text = environment.strings.Channel_AddUserKickedError - } - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - dismissController?() - - if component.call.invitePeer(peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } else if case let .legacyGroup(groupPeer) = groupPeer { - guard let selfController = environment.controller() else { - return - } - let inviteDisposable = self.inviteDisposable - var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in - dismissController?() - guard let self, let component = self.component, let environment = self.environment else { - return - } - let context = component.call.accountContext - - switch error { - case .privacy: - let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - }) - case .notMutualContact: - environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - case .tooManyChannels: - environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - case .groupFull, .generic: - environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - } - }, completed: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - dismissController?() - - if component.call.invitePeer(peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } - })]), in: .window(.root)) - } - } - }) - controller.copyInviteLink = { [weak self] in - dismissController?() - - guard let self, let component = self.component else { - return - } - let callPeerId = component.call.peerId - - let _ = (component.call.accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) - ) - |> map { peer, exportedInvitation -> String? in - if let link = inviteLinks?.listenerLink { - return link - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return "https://t.me/\(addressName)" - } else if let link = exportedInvitation?.link { - return link - } else { - return nil - } - } - |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let environment = self.environment else { - return - } - - if let link { - UIPasteboard.general.string = link - - self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) - } - }) + } + + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) + |> deliverOnMainQueue).start(next: { [weak self] chatPeer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let callState = self.callState, let peer = self.peer else { + return + } + + let initialTitle = callState.title + + let title: String + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + title = environment.strings.LiveStream_EditTitle + text = environment.strings.LiveStream_EditTitleText + } else { + title = environment.strings.VoiceChat_EditTitle + text = environment.strings.VoiceChat_EditTitleText + } + + let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let title = title, title != initialTitle else { + return } - dismissController = { [weak controller] in - controller?.dismiss() + + component.call.updateTitle(title) + + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = title.isEmpty ? environment.strings.LiveStream_EditTitleRemoveSuccess : environment.strings.LiveStream_EditTitleSuccess(title).string + } else { + text = title.isEmpty ? environment.strings.VoiceChat_EditTitleRemoveSuccess : environment.strings.VoiceChat_EditTitleSuccess(title).string } - environment.controller()?.push(controller) + + self.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) }) - case .shareLink: - guard let inviteLinks = self.inviteLinks else { - return + environment.controller()?.present(controller, in: .window(.root)) + }) + } + + func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) { + guard let component = self.component, let environment = self.environment else { + return + } + var animateInAsReplacement = false + environment.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + animateInAsReplacement = true + c.dismiss() } - self.presentShare(inviteLinks) + return true } + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current) } - private func presentShare(_ inviteLinks: GroupCallInviteLinks) { + func presentShare(_ inviteLinks: GroupCallInviteLinks) { guard let component = self.component else { return } @@ -1979,12 +604,142 @@ private final class VideoChatScreenComponent: Component { } } + private func onLeavePressed() { + guard let component = self.component, let environment = self.environment else { + return + } + + //TODO:release + let isScheduled = !"".isEmpty + + let action: (Bool) -> Void = { [weak self] terminateIfPossible in + guard let self, let component = self.component else { + return + } + + let _ = component.call.leave(terminateIfPossible: terminateIfPossible).startStandalone() + + if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { + controller.dismiss(closing: true, manual: false) + } + } + + if let callState = self.callState, callState.canManageCall { + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + let leaveTitle: String + let leaveAndCancelTitle: String + + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + leaveTitle = environment.strings.LiveStream_LeaveConfirmation + leaveAndCancelTitle = isScheduled ? environment.strings.LiveStream_LeaveAndCancelVoiceChat : environment.strings.LiveStream_LeaveAndEndVoiceChat + } else { + leaveTitle = environment.strings.VoiceChat_LeaveConfirmation + leaveAndCancelTitle = isScheduled ? environment.strings.VoiceChat_LeaveAndCancelVoiceChat : environment.strings.VoiceChat_LeaveAndEndVoiceChat + } + + items.append(ActionSheetTextItem(title: leaveTitle)) + items.append(ActionSheetButtonItem(title: leaveAndCancelTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component, let environment = self.environment else { + return + } + let title: String + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle + text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText + } else { + title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle + text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText + } + + if let _ = self.members { + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { + action(true) + })]) + environment.controller()?.present(alertController, in: .window(.root)) + } else { + action(true) + } + })) + + let leaveText: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + leaveText = environment.strings.LiveStream_LeaveVoiceChat + } else { + leaveText = environment.strings.VoiceChat_LeaveVoiceChat + } + + items.append(ActionSheetButtonItem(title: leaveText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + action(false) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + } else { + action(false) + } + } + + private func onVisibleParticipantsUpdated(ids: Set) { + if self.visibleParticipants == ids { + return + } + self.visibleParticipants = ids + self.updateTitleSpeakingStatus() + } + + private func updateTitleSpeakingStatus() { + guard let titleView = self.title.view as? VideoChatTitleComponent.View else { + return + } + + if self.speakingParticipantPeers.isEmpty { + titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2)) + } else { + var titleSpeakingStatusValue = "" + for participant in self.speakingParticipantPeers { + if !self.visibleParticipants.contains(participant.id) { + if !titleSpeakingStatusValue.isEmpty { + titleSpeakingStatusValue.append(", ") + } + titleSpeakingStatusValue.append(participant.compactDisplayTitle) + } + } + if titleSpeakingStatusValue.isEmpty { + titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2)) + } else { + titleView.updateActivityStatus(value: titleSpeakingStatusValue, transition: .easeInOut(duration: 0.2)) + } + } + } + func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } + let alphaTransition: ComponentTransition + if transition.animation.isImmediate { + alphaTransition = .immediate + } else { + alphaTransition = .easeInOut(duration: 0.25) + } + let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -2056,25 +811,7 @@ private final class VideoChatScreenComponent: Component { #endif if let membersValue = members { - var participants = membersValue.participants - participants = participants.sorted(by: { lhs, rhs in - guard let lhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == lhs.peer.id }) else { - return false - } - guard let rhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == rhs.peer.id }) else { - return false - } - - if let lhsActivityRank = lhs.activityRank, let rhsActivityRank = rhs.activityRank { - if lhsActivityRank != rhsActivityRank { - return lhsActivityRank < rhsActivityRank - } - } else if (lhs.activityRank == nil) != (rhs.activityRank == nil) { - return lhs.activityRank != nil - } - - return lhsIndex < rhsIndex - }) + let participants = membersValue.participants members = PresentationGroupCallMembers( participants: participants, speakingParticipants: membersValue.speakingParticipants, @@ -2085,8 +822,26 @@ private final class VideoChatScreenComponent: Component { self.members = members + if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden { + var videoCount = 0 + for participant in members.participants { + if participant.presentationDescription != nil { + videoCount += 1 + } + if participant.videoDescription != nil { + videoCount += 1 + } + } + if videoCount == 1, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View, let participantsComponent = participantsView.component { + if participantsComponent.layout.videoColumn != nil { + self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 + } + } + } + if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members { - if !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in + if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in if let callState = self.callState, participant.peer.id == callState.myPeerId { return false } @@ -2103,6 +858,7 @@ private final class VideoChatScreenComponent: Component { } else { self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } } @@ -2135,16 +891,32 @@ private final class VideoChatScreenComponent: Component { } else { self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } else { self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 } } else { self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } + + var speakingParticipantPeers: [EnginePeer] = [] + if let members, !members.speakingParticipants.isEmpty { + for participant in members.participants { + if members.speakingParticipants.contains(participant.peer.id) { + speakingParticipantPeers.append(EnginePeer(participant.peer)) + } + } + } + if self.speakingParticipantPeers != speakingParticipantPeers { + self.speakingParticipantPeers = speakingParticipantPeers + self.updateTitleSpeakingStatus() + } } }) @@ -2239,6 +1011,31 @@ private final class VideoChatScreenComponent: Component { } self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) }) + + self.memberEventsDisposable = (component.call.memberEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let self, let members = self.members, let component = self.component, let environment = self.environment else { + return + } + if event.joined { + var displayEvent = false + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + displayEvent = false + } + if members.totalCount < 40 { + displayEvent = true + } else if event.peer.isVerified { + displayEvent = true + } else if event.isContact || event.isInChatList { + displayEvent = true + } + + if displayEvent { + let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + } + }) } self.isPresentedValue.set(environment.isVisible) @@ -2297,16 +1094,20 @@ private final class VideoChatScreenComponent: Component { } var containerOffset: CGFloat = 0.0 - if let panGestureState = self.panGestureState { - containerOffset = panGestureState.offsetFraction * availableSize.height - self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius + if let verticalPanState = self.verticalPanState { + if verticalPanState.scrollView != nil { + containerOffset = verticalPanState.accumulatedOffset + } else { + containerOffset = verticalPanState.fraction * availableSize.height + } + self.containerView.layer.cornerRadius = containerOffset.isZero ? 0.0 : environment.deviceMetrics.screenCornerRadius } transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: availableSize), completion: { [weak self] completed in guard let self, completed else { return } - if self.panGestureState == nil { + if self.verticalPanState == nil { self.containerView.layer.cornerRadius = 0.0 } if self.notifyDismissedInteractivelyOnPanGestureApply { @@ -2353,6 +1154,7 @@ private final class VideoChatScreenComponent: Component { size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter) )), effectAlignment: .center, + minSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), action: { [weak self] in guard let self else { return @@ -2375,6 +1177,7 @@ private final class VideoChatScreenComponent: Component { size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter) )), effectAlignment: .center, + minSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), action: { [weak self] in guard let self else { return @@ -2402,7 +1205,7 @@ private final class VideoChatScreenComponent: Component { transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame) } - if isTwoColumnLayout, !"".isEmpty { + if isTwoColumnLayout { var navigationSidebarButtonTransition = transition let navigationSidebarButton: ComponentView if let current = self.navigationSidebarButton { @@ -2419,25 +1222,19 @@ private final class VideoChatScreenComponent: Component { name: "Call/PanelIcon", tintColor: .white )), - background: AnyComponent(Circle( - fillColor: UIColor(white: 1.0, alpha: 0.1), - size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter) + background: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 1.0, alpha: 0.1), + cornerRadius: .value(navigationButtonDiameter * 0.5), + smoothCorners: false )), effectAlignment: .center, + minSize: CGSize(width: navigationButtonDiameter + 10.0, height: navigationButtonDiameter), action: { [weak self] in guard let self else { return } - if let expandedParticipantsVideoState = self.expandedParticipantsVideoState { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState( - mainParticipant: expandedParticipantsVideoState.mainParticipant, - isMainParticipantPinned: expandedParticipantsVideoState.isMainParticipantPinned, - isUIHidden: !expandedParticipantsVideoState.isUIHidden - ) - self.state?.updated(transition: .spring(duration: 0.4)) - } else { - - } + self.isTwoColumnSidebarHidden = !self.isTwoColumnSidebarHidden + self.state?.updated(transition: .spring(duration: 0.4)) } )), environment: {}, @@ -2445,12 +1242,18 @@ private final class VideoChatScreenComponent: Component { ) let navigationSidebarButtonFrame = CGRect(origin: CGPoint(x: navigationRightButtonFrame.minX - 32.0 - navigationSidebarButtonSize.width, y: topInset + floor((navigationBarHeight - navigationSidebarButtonSize.height) * 0.5)), size: navigationSidebarButtonSize) if let navigationSidebarButtonView = navigationSidebarButton.view { + var animateIn = false if navigationSidebarButtonView.superview == nil { + animateIn = true if let navigationRightButtonView = self.navigationRightButton.view { self.containerView.insertSubview(navigationSidebarButtonView, aboveSubview: navigationRightButtonView) } } - transition.setFrame(view: navigationSidebarButtonView, frame: navigationSidebarButtonFrame) + navigationSidebarButtonTransition.setFrame(view: navigationSidebarButtonView, frame: navigationSidebarButtonFrame) + if animateIn { + transition.animateScale(view: navigationSidebarButtonView, from: 0.001, to: 1.0) + transition.animateAlpha(view: navigationSidebarButtonView, from: 0.0, to: 1.0) + } } } else if let navigationSidebarButton = self.navigationSidebarButton { self.navigationSidebarButton = nil @@ -2463,20 +1266,68 @@ private final class VideoChatScreenComponent: Component { } let idleTitleStatusText: String - if let callState = self.callState, callState.networkState == .connected, let members = self.members { - idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) + if let callState = self.callState { + if callState.networkState == .connected, let members = self.members { + idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) + } else if callState.scheduleTimestamp != nil { + idleTitleStatusText = environment.strings.VoiceChat_Scheduled + } else { + idleTitleStatusText = environment.strings.VoiceChat_Connecting + } } else { - idleTitleStatusText = "connecting..." + idleTitleStatusText = " " + } + + let canManageCall = self.callState?.canManageCall ?? false + + var maxTitleWidth: CGFloat = availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0 + if isTwoColumnLayout { + maxTitleWidth -= 110.0 } + let titleSize = self.title.update( transition: transition, component: AnyComponent(VideoChatTitleComponent( title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ", status: idleTitleStatusText, - strings: environment.strings + isRecording: self.callState?.recordingStartTimestamp != nil, + strings: environment.strings, + tapAction: self.callState?.recordingStartTimestamp != nil ? { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let titleView = self.title.view as? VideoChatTitleComponent.View, let recordingIndicatorView = titleView.recordingIndicatorView else { + return + } + var hasTooltipAlready = false + environment.controller()?.forEachController { controller -> Bool in + if controller is TooltipScreen { + hasTooltipAlready = true + } + return true + } + if !hasTooltipAlready { + let location = recordingIndicatorView.convert(recordingIndicatorView.bounds, to: self) + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingInProgress + } else { + text = environment.strings.VoiceChat_RecordingInProgress + } + environment.controller()?.present(TooltipScreen(account: component.call.accountContext.account, sharedContext: component.call.accountContext.sharedContext, text: .plain(text: text), icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _, _ in + return .dismiss(consume: true) + }), in: .current) + } + } : nil, + longTapAction: canManageCall ? { [weak self] in + guard let self else { + return + } + self.openTitleEditing() + } : nil )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0, height: 100.0) + containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { @@ -2530,16 +1381,34 @@ private final class VideoChatScreenComponent: Component { } } - let buttonsSideInset: CGFloat = 42.0 + let buttonsSideInset: CGFloat = 26.0 let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth - let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) + + let effectiveMaxActionMicrophoneButtonSpacing: CGFloat + if areButtonsCollapsed { + effectiveMaxActionMicrophoneButtonSpacing = 80.0 + } else { + effectiveMaxActionMicrophoneButtonSpacing = maxActionMicrophoneButtonSpacing + } + + let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) + + var isMainColumnHidden = false if isTwoColumnLayout { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden { + isMainColumnHidden = true + } else if self.isTwoColumnSidebarHidden { + isMainColumnHidden = true + } + } + + if isTwoColumnLayout { + if isMainColumnHidden { collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + sideInset + mainColumnWidth } else { collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) @@ -2591,7 +1460,8 @@ private final class VideoChatScreenComponent: Component { width: mainColumnWidth, insets: mainColumnInsets ), - columnSpacing: columnSpacing + columnSpacing: columnSpacing, + isMainColumnHidden: self.isTwoColumnSidebarHidden ) } else { let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset) @@ -2601,7 +1471,8 @@ private final class VideoChatScreenComponent: Component { width: mainColumnWidth, insets: mainColumnInsets ), - columnSpacing: columnSpacing + columnSpacing: columnSpacing, + isMainColumnHidden: false ) } @@ -2633,7 +1504,7 @@ private final class VideoChatScreenComponent: Component { component: AnyComponent(VideoChatParticipantsComponent( call: component.call, participants: mappedParticipants, - speakingParticipants: members?.speakingParticipants ?? Set(), + speakingParticipants: self.members?.speakingParticipants ?? Set(), expandedVideoState: self.expandedParticipantsVideoState, theme: environment.theme, strings: environment.strings, @@ -2647,7 +1518,7 @@ private final class VideoChatScreenComponent: Component { } self.openParticipantContextMenu(id: id, sourceView: sourceView, gesture: gesture) }, - updateMainParticipant: { [weak self] key in + updateMainParticipant: { [weak self] key, alsoSetIsUIHidden in guard let self else { return } @@ -2655,7 +1526,14 @@ private final class VideoChatScreenComponent: Component { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key { return } - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: self.expandedParticipantsVideoState?.isUIHidden ?? false) + + var isUIHidden = self.expandedParticipantsVideoState?.isUIHidden ?? false + if let alsoSetIsUIHidden { + isUIHidden = alsoSetIsUIHidden + } + + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: isUIHidden) + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 3.0 self.state?.updated(transition: .spring(duration: 0.4)) } else if self.expandedParticipantsVideoState != nil { self.expandedParticipantsVideoState = nil @@ -2701,6 +1579,12 @@ private final class VideoChatScreenComponent: Component { return } self.openInviteMembers() + }, + visibleParticipantsUpdated: { [weak self] visibleParticipants in + guard let self else { + return + } + self.onVisibleParticipantsUpdated(ids: visibleParticipants) } )), environment: {}, @@ -2709,35 +1593,88 @@ private final class VideoChatScreenComponent: Component { let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: participantsSize) if let participantsView = self.participants.view { if participantsView.superview == nil { + participantsView.layer.allowsGroupOpacity = true self.containerView.addSubview(participantsView) } transition.setFrame(view: participantsView, frame: participantsFrame) + var participantsAlpha: CGFloat = 1.0 + if let callState = self.callState, callState.scheduleTimestamp != nil { + participantsAlpha = 0.0 + } + alphaTransition.setAlpha(view: participantsView, alpha: participantsAlpha) + } + + if let callState = self.callState, let scheduleTimestamp = callState.scheduleTimestamp { + let scheduleInfo: ComponentView + var scheduleInfoTransition = transition + if let current = self.scheduleInfo { + scheduleInfo = current + } else { + scheduleInfoTransition = scheduleInfoTransition.withAnimation(.none) + scheduleInfo = ComponentView() + self.scheduleInfo = scheduleInfo + } + let scheduleInfoSize = scheduleInfo.update( + transition: scheduleInfoTransition, + component: AnyComponent(VideoChatScheduledInfoComponent( + timestamp: scheduleTimestamp, + strings: environment.strings + )), + environment: {}, + containerSize: participantsSize + ) + let scheduleInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scheduleInfoSize) + if let scheduleInfoView = scheduleInfo.view { + if scheduleInfoView.superview == nil { + scheduleInfoView.isUserInteractionEnabled = false + self.containerView.addSubview(scheduleInfoView) + } + scheduleInfoTransition.setFrame(view: scheduleInfoView, frame: scheduleInfoFrame) + } + } else if let scheduleInfo = self.scheduleInfo { + self.scheduleInfo = nil + if let scheduleInfoView = scheduleInfo.view { + alphaTransition.setAlpha(view: scheduleInfoView, alpha: 0.0, completion: { [weak scheduleInfoView] _ in + scheduleInfoView?.removeFromSuperview() + }) + } } let micButtonContent: VideoChatMicButtonComponent.Content let actionButtonMicrophoneState: VideoChatActionButtonComponent.MicrophoneState if let callState = self.callState { - switch callState.networkState { - case .connecting: - micButtonContent = .connecting - actionButtonMicrophoneState = .connecting - case .connected: - if let callState = callState.muteState { - if callState.canUnmute { - if self.isPushToTalkActive { - micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) - actionButtonMicrophoneState = .unmuted + if callState.scheduleTimestamp != nil { + let scheduledState: VideoChatMicButtonComponent.ScheduledState + if callState.canManageCall { + scheduledState = .start + } else { + scheduledState = .toggleSubscription(isSubscribed: callState.subscribedToScheduled) + } + micButtonContent = .scheduled(state: scheduledState) + actionButtonMicrophoneState = .scheduled + } else { + switch callState.networkState { + case .connecting: + micButtonContent = .connecting + actionButtonMicrophoneState = .connecting + case .connected: + if let muteState = callState.muteState { + if muteState.canUnmute { + if self.isPushToTalkActive { + micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) + actionButtonMicrophoneState = .unmuted + } else { + micButtonContent = .muted + actionButtonMicrophoneState = .muted + } } else { - micButtonContent = .muted - actionButtonMicrophoneState = .muted + micButtonContent = .raiseHand(isRaised: callState.raisedHand) + actionButtonMicrophoneState = .raiseHand } } else { - micButtonContent = .raiseHand - actionButtonMicrophoneState = .raiseHand + micButtonContent = .unmuted(pushToTalk: false) + actionButtonMicrophoneState = .unmuted } - } else { - micButtonContent = .unmuted(pushToTalk: false) - actionButtonMicrophoneState = .unmuted } } } else { @@ -2749,6 +1686,7 @@ private final class VideoChatScreenComponent: Component { transition: transition, component: AnyComponent(VideoChatMicButtonComponent( call: component.call, + strings: environment.strings, content: micButtonContent, isCollapsed: areButtonsCollapsed, updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in @@ -2797,6 +1735,23 @@ private final class VideoChatScreenComponent: Component { if !callState.raisedHand { component.call.raiseHand() } + }, + scheduleAction: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard callState.scheduleTimestamp != nil else { + return + } + + if callState.canManageCall { + component.call.startScheduled() + } else { + component.call.toggleScheduledSubscription(!callState.subscribedToScheduled) + } } )), environment: {}, @@ -2813,7 +1768,9 @@ private final class VideoChatScreenComponent: Component { let videoButtonContent: VideoChatActionButtonComponent.Content if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute { var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker + var buttonIsEnabled = false if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput { + buttonIsEnabled = availableOutputs.count > 1 switch currentOutput { case .builtin: buttonAudio = .builtin @@ -2837,7 +1794,7 @@ private final class VideoChatScreenComponent: Component { buttonAudio = .none } } - videoButtonContent = .audio(audio: buttonAudio) + videoButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled) } else { //TODO:release videoButtonContent = .video(isActive: false) @@ -2886,14 +1843,10 @@ private final class VideoChatScreenComponent: Component { )), effectAlignment: .center, action: { [weak self] in - guard let self, let component = self.component else { + guard let self else { return } - let _ = component.call.leave(terminateIfPossible: false).startStandalone() - - if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { - controller.dismiss(closing: true, manual: false) - } + self.onLeavePressed() }, animateAlpha: false )), @@ -3046,9 +1999,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo } self.isAnimatingDismiss = false self.superDismiss() + completion?() }) } else { self.superDismiss() + completion?() } } } @@ -3074,25 +2029,3 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo } } } - -private final class ParticipantExtractedContentSource: ContextExtractedContentSource { - let keepInPlace: Bool = false - let ignoreContentTouches: Bool = false - let blurBackground: Bool = true - - //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center - - private let contentView: ContextExtractedContentContainingView - - init(contentView: ContextExtractedContentContainingView) { - self.contentView = contentView - } - - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) - } -} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift new file mode 100644 index 00000000000..70e52ce32e5 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -0,0 +1,343 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SwiftSignalKit +import PeerInfoUI +import OverlayStatusController +import PresentationDataUtils + +extension VideoChatScreenComponent.View { + func openInviteMembers() { + guard let component = self.component else { + return + } + + var canInvite = true + var inviteIsLink = false + if case let .channel(peer) = self.peer { + if peer.flags.contains(.isGigagroup) { + if peer.flags.contains(.isCreator) || peer.adminRights != nil { + } else { + canInvite = false + } + } + if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { + inviteIsLink = true + } + } + var inviteType: VideoChatParticipantsComponent.Participants.InviteType? + if canInvite { + if inviteIsLink { + inviteType = .shareLink + } else { + inviteType = .invite + } + } + + guard let inviteType else { + return + } + + switch inviteType { + case .invite: + let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId)) + let _ = (groupPeer + |> deliverOnMainQueue).start(next: { [weak self] groupPeer in + guard let self, let component = self.component, let environment = self.environment, let groupPeer else { + return + } + let inviteLinks = self.inviteLinks + + if case let .channel(groupPeer) = groupPeer { + var canInviteMembers = true + if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { + canInviteMembers = false + } + if !canInviteMembers { + if let inviteLinks { + self.presentShare(inviteLinks) + } + return + } + } + + var filters: [ChannelMembersSearchFilter] = [] + if let members = self.members { + filters.append(.disable(Array(members.participants.map { $0.peer.id }))) + } + if case let .channel(groupPeer) = groupPeer { + if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { + filters.append(.excludeNonMembers) + } + } else if case let .legacyGroup(groupPeer) = groupPeer { + if groupPeer.hasBannedPermission(.banAddMembers) { + filters.append(.excludeNonMembers) + } + } + filters.append(.excludeBots) + + var dismissController: (() -> Void)? + let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + guard let callState = self.callState else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + if peer.id == callState.myPeerId { + return + } + if let participant { + dismissController?() + + if component.call.invitePeer(participant.peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + } else { + if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { + let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + + let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + }) + })]), in: .window(.root)) + } else { + let text: String + if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { + text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + if case let .channel(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id]) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = environment.strings.Channel_ErrorAddTooMuch + case .tooMuchJoined: + text = environment.strings.Invite_ChannelsTooMuch + case .generic: + text = environment.strings.Login_UnknownError + case .restricted: + text = environment.strings.Channel_ErrorAddBlocked + case .notMutualContact: + if case .broadcast = groupPeer.info { + text = environment.strings.Channel_AddUserLeftError + } else { + text = environment.strings.GroupInfo_AddUserLeftError + } + case .botDoesntSupportGroups: + text = environment.strings.Channel_BotDoesntSupportGroups + case .tooMuchBots: + text = environment.strings.Channel_TooMuchBots + case .bot: + text = environment.strings.Login_UnknownError + case .kicked: + text = environment.strings.Channel_AddUserKickedError + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } else if case let .legacyGroup(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + let context = component.call.accountContext + + switch error { + case .privacy: + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }) + case .notMutualContact: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .tooManyChannels: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .groupFull, .generic: + environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + } + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } + })]), in: .window(.root)) + } + } + }) + controller.copyInviteLink = { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + let callPeerId = component.call.peerId + + let _ = (component.call.accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), + TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) + ) + |> map { peer, exportedInvitation -> String? in + if let link = inviteLinks?.listenerLink { + return link + } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { + return "https://t.me/\(addressName)" + } else if let link = exportedInvitation?.link { + return link + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let environment = self.environment else { + return + } + + if let link { + UIPasteboard.general.string = link + + self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) + } + }) + } + dismissController = { [weak controller] in + controller?.dismiss() + } + environment.controller()?.push(controller) + }) + case .shareLink: + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift new file mode 100644 index 00000000000..16ecfecd1b5 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -0,0 +1,560 @@ +import Foundation +import UIKit +import Display +import ContextUI +import TelegramCore +import SwiftSignalKit +import DeleteChatPeerActionSheetItem +import PeerListItemComponent +import LegacyComponents +import LegacyUI +import WebSearchUI +import MapResourceToAvatarSizes +import LegacyMediaPickerUI +import AvatarNode +import PresentationDataUtils +import AccountContext + +extension VideoChatScreenComponent.View { + func openMoreMenu() { + guard let sourceView = self.navigationLeftButton.view else { + return + } + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + guard let peer = self.peer else { + return + } + guard let callState = self.callState else { + return + } + + let canManageCall = callState.canManageCall + + var items: [ContextMenuItem] = [] + + if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { + for peer in displayAsPeers { + if peer.peer.id == callState.myPeerId { + let avatarSize = CGSize(width: 28.0, height: 28.0) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuDisplayAsItems())))) + }))) + items.append(.separator) + break + } + } + } + + if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { + var currentOutputTitle = "" + for output in availableOutputs { + if output == currentOutput { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = environment.strings.Call_AudioRouteSpeaker + case .headphones: + title = environment.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + currentOutputTitle = title + break + } + } + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuAudioItems())))) + }))) + } + + if canManageCall { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_EditTitle + } else { + text = environment.strings.VoiceChat_EditTitle + } + items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.openTitleEditing() + }))) + + var hasPermissions = true + if case let .channel(chatPeer) = peer { + if case .broadcast = chatPeer.info { + hasPermissions = false + } else if chatPeer.flags.contains(.isGigagroup) { + hasPermissions = false + } + } + if hasPermissions { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuPermissionItems())))) + }))) + } + } + + if let inviteLinks = self.inviteLinks { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.presentShare(inviteLinks) + }))) + } + + //let isScheduled = strongSelf.isScheduled + //TODO:release + let isScheduled: Bool = !"".isEmpty + + let canSpeak: Bool + if let muteState = callState.muteState { + canSpeak = muteState.canUnmute + } else { + canSpeak = true + } + + if !isScheduled && canSpeak { + if #available(iOS 15.0, *) { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + AVCaptureDevice.showSystemUserInterface(.microphoneModes) + }))) + } + } + + if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { + if component.call.hasScreencast { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + component.call.disableScreencast() + }))) + } else { + items.append(.custom(VoiceChatShareScreenContextItem(context: component.call.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { _, _ in }), false)) + } + } + + if canManageCall { + if let recordingStartTimestamp = callState.recordingStartTimestamp { + items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + component.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) + + Queue.mainQueue().after(0.88) { + HapticFeedback().success() + } + + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingSaved + } else { + text = environment.strings.VideoChat_RecordingSaved + } + self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in + if case .info = value, let self, let component = self.component, let environment = self.environment, let navigationController = environment.controller()?.navigationController as? NavigationController { + let context = component.call.accountContext + environment.controller()?.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().justDispatch { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer, let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + } + }) + + return true + } + return false + }) + })]) + environment.controller()?.present(alertController, in: .window(.root)) + }), false)) + } else { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_StartRecording + } else { + text = environment.strings.VoiceChat_StartRecording + } + if callState.scheduleTimestamp == nil { + items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { + return + } + + let controller = VoiceChatRecordingSetupController(context: component.call.accountContext, peer: peer, completion: { [weak self] videoOrientation in + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { + return + } + let title: String + let text: String + let placeholder: String + if let _ = videoOrientation { + placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholderVideo + } else { + placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholder + } + if case let .channel(channel) = peer, case .broadcast = channel.info { + title = environment.strings.LiveStream_StartRecordingTitle + if let _ = videoOrientation { + text = environment.strings.LiveStream_StartRecordingTextVideo + } else { + text = environment.strings.LiveStream_StartRecordingText + } + } else { + title = environment.strings.VoiceChat_StartRecordingTitle + if let _ = videoOrientation { + text = environment.strings.VoiceChat_StartRecordingTextVideo + } else { + text = environment.strings.VoiceChat_StartRecordingText + } + } + + let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer, let title else { + return + } + + component.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) + + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingStarted + } else { + text = environment.strings.VoiceChat_RecordingStarted + } + + self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) + component.call.playTone(.recordingStarted) + }) + environment.controller()?.present(controller, in: .window(.root)) + }) + environment.controller()?.present(controller, in: .window(.root)) + }))) + } + } + } + + if canManageCall { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream + } else { + text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat + } + items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let action: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = (component.call.leave(terminateIfPossible: true) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let environment = self.environment else { + return + } + environment.controller()?.dismiss() + }) + } + + let title: String + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle + text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText + } else { + title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle + text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText + } + + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { + action() + })]) + environment.controller()?.present(alertController, in: .window(.root)) + }))) + } else { + let leaveText: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + leaveText = environment.strings.LiveStream_LeaveVoiceChat + } else { + leaveText = environment.strings.VoiceChat_LeaveVoiceChat + } + items.append(.action(ContextMenuActionItem(text: leaveText, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + + let _ = (component.call.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let environment = self.environment else { + return + } + environment.controller()?.dismiss() + }) + }))) + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + private func contextMenuDisplayAsItems() -> [ContextMenuItem] { + guard let component = self.component, let environment = self.environment else { + return [] + } + guard let callState = self.callState else { + return [] + } + let myPeerId = callState.myPeerId + + let avatarSize = CGSize(width: 28.0, height: 28.0) + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + + var isGroup = false + if let displayAsPeers = self.displayAsPeers { + for peer in displayAsPeers { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + isGroup = true + break + } + } + } + + items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? environment.strings.VoiceChat_DisplayAsInfoGroup : environment.strings.VoiceChat_DisplayAsInfo, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) + }), true)) + + if let displayAsPeers = self.displayAsPeers { + for peer in displayAsPeers { + var subtitle: String? + if peer.peer.id.namespace == Namespaces.Peer.CloudUser { + subtitle = environment.strings.VoiceChat_PersonalAccount + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = environment.strings.Conversation_StatusMembers(subscribers) + } + } + + let isSelected = peer.peer.id == myPeerId + let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) + let theme = environment.theme + let avatarSignal = peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) + |> map { image -> UIImage? in + if isSelected, let image = image { + return generateImage(extendedAvatarSize, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) + + let lineWidth = 1.0 + UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.actionSheet.controlAccentColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + }) + } else { + return image + } + } + + items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + if peer.peer.id != myPeerId { + component.call.reconnect(as: peer.peer.id) + } + }))) + + if peer.peer.id.namespace == Namespaces.Peer.CloudUser { + items.append(.separator) + } + } + } + return items + } + + private func contextMenuAudioItems() -> [ContextMenuItem] { + guard let environment = self.environment else { + return [] + } + guard let (availableOutputs, currentOutput) = self.audioOutputState else { + return [] + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + + for output in availableOutputs { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = environment.strings.Call_AudioRouteSpeaker + case .headphones: + title = environment.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + if output == currentOutput { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + component.call.setCurrentAudioOutput(output) + }))) + } + + return items + } + + private func contextMenuPermissionItems() -> [ContextMenuItem] { + guard let environment = self.environment, let callState = self.callState else { + return [] + } + + var items: [ContextMenuItem] = [] + if callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + let isMuted = defaultParticipantMuteState == .muted + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in + if isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + component.call.updateDefaultParticipantsAreMuted(isMuted: false) + }))) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in + if !isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + component.call.updateDefaultParticipantsAreMuted(isMuted: true) + }))) + } + return items + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift new file mode 100644 index 00000000000..dd81e23a91c --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -0,0 +1,667 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramCore +import ContextUI +import DeleteChatPeerActionSheetItem +import UndoUI +import LegacyComponents +import WebSearchUI +import MapResourceToAvatarSizes +import LegacyUI +import LegacyMediaPickerUI + +extension VideoChatScreenComponent.View { + func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { + guard let component = self.component, let environment = self.environment else { + return + } + guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else { + return + } + + let muteStatePromise = Promise(participant.muteState) + + let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in + guard let self, let component = self.component, let environment = self.environment else { + return [] + } + guard let callState = self.callState else { + return [] + } + + var items: [ContextMenuItem] = [] + + var hasVolumeSlider = false + let peer = participant.peer + if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { + } else { + if callState.canManageCall || callState.myPeerId != id { + hasVolumeSlider = true + + let minValue: CGFloat + if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { + minValue = 0.01 + } else { + minValue = 0.0 + } + items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in + guard let self, let component = self.component else { + return + } + + if finished && newValue.isZero { + let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true) + muteStatePromise.set(.single(updatedMuteState)) + } else { + component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) + } + }), true)) + } + } + + if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) { + items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) + }), true)) + } + + if peer.id == callState.myPeerId { + if participant.hasRaiseHand { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + component.call.lowerHand() + + f(.default) + }))) + } + items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + Queue.mainQueue().after(0.1) { + guard let self else { + return + } + + self.openAvatarForEditing(fromGallery: false, completion: {}) + } + }))) + + items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + Queue.mainQueue().after(0.1) { + guard let self, let component = self.component, let environment = self.environment else { + return + } + let maxBioLength: Int + if peer.id.namespace == Namespaces.Peer.CloudUser { + maxBioLength = 70 + } else { + maxBioLength = 100 + } + let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in + guard let self, let component = self.component, let environment = self.environment, let bio else { + return + } + if peer.id.namespace == Namespaces.Peer.CloudUser { + let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } else { + let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } + + self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + }) + environment.controller()?.present(controller, in: .window(.root)) + } + }))) + + if let peer = peer as? TelegramUser { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + Queue.mainQueue().after(0.1) { + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in + guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else { + return + } + let _ = component.call.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() + + self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + }) + environment.controller()?.present(controller, in: .window(.root)) + } + }))) + } + } else { + if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) { + if callState.adminIds.contains(peer.id) { + if let _ = muteState { + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } else { + if let muteState = muteState, !muteState.canUnmute { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) + f(.default) + + self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } + } else { + if let muteState = muteState, muteState.mutedByYou { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) + f(.default) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } + + let openTitle: String + let openIcon: UIImage? + if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + openTitle = environment.strings.VoiceChat_OpenChannel + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") + } else { + openTitle = environment.strings.VoiceChat_OpenGroup + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") + } + } else { + openTitle = environment.strings.Conversation_ContextMenuSendMessage + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") + } + items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in + return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { + return + } + + let context = component.call.accountContext + controller.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().after(0.1) { + guard let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + + f(.dismissWithoutContent) + }))) + + if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self, let component = self.component else { + return + } + + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) + |> deliverOnMainQueue).start(next: { [weak self] chatPeer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + let nameDisplayOrder = presentationData.nameDisplayOrder + items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) + + items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: component.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() + component.call.removedPeer(peer.id) + + self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + }) + }) + }))) + } + } + return items + } + + let items = muteStatePromise.get() + |> map { muteState -> [ContextMenuItem] in + return itemsForEntry(muteState) + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), + items: items |> map { items in + return ContextController.Items(content: .list(items)) + }, + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + + environment.controller()?.presentInGlobalOverlay(contextController) + } + + private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + let peerId = callState.myPeerId + + let _ = (component.call.accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Configuration.SearchBots() + ) + |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let peer else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let legacyController = LegacyController(presentation: .custom, theme: environment.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + self.endEditing(true) + environment.controller()?.present(legacyController, in: .window(.root)) + + var hasPhotos = false + if !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! + mixin.forceDark = true + mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext) + let _ = self.currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { [weak self] assetsController in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in + assetsController?.dismiss() + + guard let self else { + return + } + self.updateProfilePhoto(result) + })) + controller.navigationPresentation = .modal + environment.controller()?.push(controller) + + if fromGallery { + completion() + } + } + mixin.didFinishWithImage = { [weak self] image in + if let image = image { + completion() + self?.updateProfilePhoto(image) + } + } + mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in + if let image = image, let asset = asset { + completion() + self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) + } + } + mixin.didFinishWithDelete = { [weak self] in + guard let self, let environment = self.environment else { + return + } + + let proceed = { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = self.currentAvatarMixin.swap(nil) + let postbox = component.call.accountContext.account.postbox + self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start()) + } + + let actionSheet = ActionSheetController(presentationData: presentationData) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + proceed() + }) + ] + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + } + mixin.didDismiss = { [weak self, weak legacyController] in + guard let self else { + return + } + let _ = self.currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) + } + + private func updateProfilePhoto(_ image: UIImage) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard let data = image.jpegData(compressionQuality: 0.6) else { + return + } + + let peerId = callState.myPeerId + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) + + self.currentUpdatingAvatar = (representation, 0.0) + + let postbox = component.call.account.postbox + let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + + self.updateAvatarDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .complete: + self.currentUpdatingAvatar = nil + self.state?.updated(transition: .spring(duration: 0.4)) + case let .progress(value): + self.currentUpdatingAvatar = (representation, value) + } + })) + + self.state?.updated(transition: .spring(duration: 0.4)) + } + + private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard let data = image.jpegData(compressionQuality: 0.6) else { + return + } + let peerId = callState.myPeerId + + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) + + self.currentUpdatingAvatar = (representation, 0.0) + + var videoStartTimestamp: Double? = nil + if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { + videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue + } + + let context = component.call.accountContext + let account = context.account + let signal = Signal { [weak self] subscriber in + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) + } else { + return nil + } + } + + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let uploadInterface = LegacyLiveUploadInterface(context: context) + let signal: SSignal + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber.putNext(duration) + subscriber.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + signal = durationSignal.map(toSignal: { duration -> SSignal in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! + } else { + signal = SSignal.complete() + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { + account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + } + + if let timestamp = videoStartTimestamp { + videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) + } + + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL) { + let resource: TelegramMediaResource + if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + } else { + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + } + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + + EngineTempBox.shared.dispose(tempFile) + } + } + subscriber.putCompletion() + } else if let progress = next as? NSNumber { + Queue.mainQueue().async { [weak self] in + guard let self else { + return + } + self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25) + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() + } + } + + self.updateAvatarDisposable.set((signal + |> mapToSignal { videoResource -> Signal in + if peerId.namespace == Namespaces.Peer.CloudUser { + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .complete: + self.currentUpdatingAvatar = nil + self.state?.updated(transition: .spring(duration: 0.4)) + case let .progress(value): + self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75) + self.state?.updated(transition: .spring(duration: 0.4)) + } + })) + } +} + +private final class ParticipantExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index 93b9115a314..b1818a7e936 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -4,20 +4,31 @@ import Display import ComponentFlow import MultilineTextComponent import TelegramPresentationData +import HierarchyTrackingLayer +import ChatTitleActivityNode final class VideoChatTitleComponent: Component { let title: String let status: String + let isRecording: Bool let strings: PresentationStrings + let tapAction: (() -> Void)? + let longTapAction: (() -> Void)? init( title: String, status: String, - strings: PresentationStrings + isRecording: Bool, + strings: PresentationStrings, + tapAction: (() -> Void)?, + longTapAction: (() -> Void)? ) { self.title = title self.status = status + self.isRecording = isRecording self.strings = strings + self.tapAction = tapAction + self.longTapAction = longTapAction } static func ==(lhs: VideoChatTitleComponent, rhs: VideoChatTitleComponent) -> Bool { @@ -27,27 +38,152 @@ final class VideoChatTitleComponent: Component { if lhs.status != rhs.status { return false } + if lhs.isRecording != rhs.isRecording { + return false + } if lhs.strings !== rhs.strings { return false } + if (lhs.tapAction == nil) != (rhs.tapAction == nil) { + return false + } + if (lhs.longTapAction == nil) != (rhs.longTapAction == nil) { + return false + } return true } final class View: UIView { + private let hierarchyTrackingLayer: HierarchyTrackingLayer private let title = ComponentView() - private var status: ComponentView? + private let status = ComponentView() + private var recordingImageView: UIImageView? + + private var activityStatusNode: ChatTitleActivityNode? private var component: VideoChatTitleComponent? private var isUpdating: Bool = false + private var currentActivityStatus: String? + private var currentSize: CGSize? + + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + + public var recordingIndicatorView: UIView? { + return self.recordingImageView + } + override init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + self.updateAnimations() + } + + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + tapRecognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.addGestureRecognizer(tapRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if case .tap = gesture { + component.tapAction?() + } else if case .longTap = gesture { + component.longTapAction?() + } + } + } + } + + private func updateAnimations() { + if let recordingImageView = self.recordingImageView { + if recordingImageView.layer.animation(forKey: "blink") == nil { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber] + animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] + animation.duration = 0.7 + animation.autoreverses = true + animation.repeatCount = Float.infinity + recordingImageView.layer.add(animation, forKey: "blink") + } + } + } + + func updateActivityStatus(value: String?, transition: ComponentTransition) { + if self.currentActivityStatus == value { + return + } + self.currentActivityStatus = value + + guard let currentSize = self.currentSize, let statusView = self.status.view else { + return + } + + let alphaTransition: ComponentTransition + if transition.animation.isImmediate { + alphaTransition = .immediate + } else { + alphaTransition = .easeInOut(duration: 0.2) + } + + if let value { + let activityStatusNode: ChatTitleActivityNode + if let current = self.activityStatusNode { + activityStatusNode = current + } else { + activityStatusNode = ChatTitleActivityNode() + self.activityStatusNode = activityStatusNode + } + + let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none) + let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center) + let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize) + + let activityStatusNodeView = activityStatusNode.view + activityStatusNodeView.center = activityStatusFrame.center + activityStatusNodeView.bounds = CGRect(origin: CGPoint(), size: activityStatusFrame.size) + if activityStatusNodeView.superview == nil { + self.addSubview(activityStatusNode.view) + ComponentTransition.immediate.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0)) + activityStatusNodeView.alpha = 0.0 + } + transition.setTransform(view: activityStatusNodeView, transform: CATransform3DIdentity) + alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 1.0) + + transition.setTransform(view: statusView, transform: CATransform3DMakeTranslation(0.0, 10.0, 0.0)) + alphaTransition.setAlpha(view: statusView, alpha: 0.0) + } else { + if let activityStatusNode = self.activityStatusNode { + self.activityStatusNode = nil + let activityStatusNodeView = activityStatusNode.view + transition.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0)) + alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 0.0, completion: { [weak activityStatusNodeView] _ in + activityStatusNodeView?.removeFromSuperview() + }) + } + + transition.setTransform(view: statusView, transform: CATransform3DIdentity) + alphaTransition.setAlpha(view: statusView, alpha: 1.0) + } + } + func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -56,30 +192,30 @@ final class VideoChatTitleComponent: Component { self.component = component + self.tapRecognizer?.isEnabled = component.longTapAction != nil || component.tapAction != nil + let spacing: CGFloat = 1.0 + var maxTitleWidth = availableSize.width + if component.isRecording { + maxTitleWidth -= 10.0 + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) + containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) - let status: ComponentView - if let current = self.status { - status = current - } else { - status = ComponentView() - self.status = status - } let statusComponent: AnyComponent statusComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5))) )) - let statusSize = status.update( + let statusSize = self.status.update( transition: .immediate, component: statusComponent, environment: {}, @@ -91,21 +227,52 @@ final class VideoChatTitleComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + titleView.isUserInteractionEnabled = false self.addSubview(titleView) } - transition.setPosition(view: titleView, position: titleFrame.center) + transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize) - if let statusView = status.view { + if let statusView = self.status.view { if statusView.superview == nil { + statusView.isUserInteractionEnabled = false self.addSubview(statusView) } transition.setPosition(view: statusView, position: statusFrame.center) statusView.bounds = CGRect(origin: CGPoint(), size: statusFrame.size) } + if component.isRecording { + var recordingImageTransition = transition + let recordingImageView: UIImageView + if let current = self.recordingImageView { + recordingImageView = current + } else { + recordingImageTransition = recordingImageTransition.withAnimation(.none) + recordingImageView = UIImageView() + recordingImageView.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xFF3B2F)) + self.recordingImageView = recordingImageView + self.addSubview(recordingImageView) + transition.animateScale(view: recordingImageView, from: 0.0001, to: 1.0) + } + let recordingImageFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: titleFrame.minY + floor(titleFrame.height - 8.0) * 0.5 + 1.0), size: CGSize(width: 8.0, height: 8.0)) + recordingImageTransition.setFrame(view: recordingImageView, frame: recordingImageFrame) + + self.updateAnimations() + } else { + if let recordingImageView = self.recordingImageView { + self.recordingImageView = nil + transition.setScale(view: recordingImageView, scale: 0.0001, completion: { [weak recordingImageView] _ in + recordingImageView?.removeFromSuperview() + }) + } + } + + self.currentSize = size + return size } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift index 8b812681487..364289cff18 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift @@ -8,125 +8,196 @@ private let shadowImage: UIImage? = { UIImage(named: "Stories/PanelGradient") }() -final class VideoChatVideoLoadingEffectView: UIView { - private let duration: Double - private let hasCustomBorder: Bool - private let playOnce: Bool +private func generateGradient(baseAlpha: CGFloat) -> UIImage? { + return generateImage(CGSize(width: 200.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) + + if let shadowImage { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + }) +} + +private final class AnimatedGradientView: UIView { + private struct Params: Equatable { + var size: CGSize + var containerWidth: CGFloat + var offsetX: CGFloat + var gradientWidth: CGFloat + + init(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat) { + self.size = size + self.containerWidth = containerWidth + self.offsetX = offsetX + self.gradientWidth = gradientWidth + } + } + private let duration: Double private let hierarchyTrackingLayer: HierarchyTrackingLayer - private let gradientWidth: CGFloat - - let portalSource: PortalSourceView - + private let backgroundContainerView: UIView + private let backgroundScaleView: UIView + private let backgroundOffsetView: UIView private let backgroundView: UIImageView - private let borderGradientView: UIImageView - private let borderContainerView: UIView - let borderMaskLayer: SimpleShapeLayer + private var params: Params? - private var didPlayOnce = false - - init(effectAlpha: CGFloat, borderAlpha: CGFloat, gradientWidth: CGFloat = 200.0, duration: Double, hasCustomBorder: Bool, playOnce: Bool) { - self.portalSource = PortalSourceView() - + init(effectAlpha: CGFloat, duration: Double) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.duration = duration - self.hasCustomBorder = hasCustomBorder - self.playOnce = playOnce - self.gradientWidth = gradientWidth - self.backgroundView = UIImageView() + self.backgroundContainerView = UIView() + self.backgroundContainerView.layer.anchorPoint = CGPoint() - self.borderGradientView = UIImageView() - self.borderContainerView = UIView() - self.borderMaskLayer = SimpleShapeLayer() + self.backgroundScaleView = UIView() + self.backgroundOffsetView = UIView() - super.init(frame: .zero) + self.backgroundView = UIImageView() - self.portalSource.backgroundColor = .red + super.init(frame: CGRect()) - self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer) + self.layer.addSublayer(self.hierarchyTrackingLayer) self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in - guard let self, self.bounds.width != 0.0 else { + guard let self else { return } - self.updateAnimations(size: self.bounds.size) + self.updateAnimations() } - let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in - return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) - - if let shadowImage { - UIGraphicsPushContext(context) - - for i in 0 ..< 2 { - let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) - - context.saveGState() - context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) - context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) - let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) - - context.clip(to: adjustedRect, mask: shadowImage.cgImage!) - context.setFillColor(foregroundColor.cgColor) - context.fill(adjustedRect) - - context.restoreGState() - } - - UIGraphicsPopContext() - } - }) - } - self.backgroundView.image = generateGradient(effectAlpha) - self.portalSource.addSubview(self.backgroundView) + self.backgroundView.image = generateGradient(baseAlpha: effectAlpha) - self.borderGradientView.image = generateGradient(borderAlpha) - self.borderContainerView.addSubview(self.borderGradientView) - self.portalSource.addSubview(self.borderContainerView) - self.borderContainerView.layer.mask = self.borderMaskLayer + self.backgroundOffsetView.addSubview(self.backgroundView) + self.backgroundScaleView.addSubview(self.backgroundOffsetView) + self.backgroundContainerView.addSubview(self.backgroundScaleView) + self.addSubview(self.backgroundContainerView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func updateAnimations(size: CGSize) { - if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) { + private func updateAnimations() { + if self.backgroundView.layer.animation(forKey: "shimmer") == nil { + let animation = self.backgroundView.layer.makeAnimation(from: -1.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.backgroundView.layer.add(animation, forKey: "shimmer") + } + if self.backgroundScaleView.layer.animation(forKey: "shimmer") == nil { + let animation = self.backgroundScaleView.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.backgroundScaleView.layer.add(animation, forKey: "shimmer") + } + } + + func update(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat, transition: ComponentTransition) { + let params = Params(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth) + if self.params == params { return } + self.params = params + + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) + transition.setPosition(view: self.backgroundView, position: backgroundFrame.center) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.setPosition(view: self.backgroundOffsetView, position: backgroundFrame.center) + transition.setBounds(view: self.backgroundOffsetView, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.setTransform(view: self.backgroundOffsetView, transform: CATransform3DMakeScale(gradientWidth, 1.0, 1.0)) + + let backgroundContainerViewSubFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + transition.setPosition(view: self.backgroundContainerView, position: CGPoint()) + transition.setBounds(view: self.backgroundContainerView, bounds: backgroundContainerViewSubFrame) + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, -offsetX, 0.0, 0.0) + containerTransform = CATransform3DScale(containerTransform, containerWidth, size.height, 1.0) + transition.setSublayerTransform(view: self.backgroundContainerView, transform: containerTransform) + + transition.setSublayerTransform(view: self.backgroundScaleView, transform: CATransform3DMakeScale(1.0 / containerWidth, 1.0, 1.0)) + + self.updateAnimations() + } +} - let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.2) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) - animation.repeatCount = self.playOnce ? 1 : Float.infinity - self.backgroundView.layer.add(animation, forKey: "shimmer") - self.borderGradientView.layer.add(animation, forKey: "shimmer") +final class VideoChatVideoLoadingEffectView: UIView { + private struct Params: Equatable { + var size: CGSize + var containerWidth: CGFloat + var offsetX: CGFloat + var gradientWidth: CGFloat - self.didPlayOnce = true + init(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat) { + self.size = size + self.containerWidth = containerWidth + self.offsetX = offsetX + self.gradientWidth = gradientWidth + } } - func update(size: CGSize, transition: ComponentTransition) { - if self.backgroundView.bounds.size != size { - self.backgroundView.layer.removeAllAnimations() - - if !self.hasCustomBorder { - self.borderMaskLayer.fillColor = nil - self.borderMaskLayer.strokeColor = UIColor.white.cgColor - let lineWidth: CGFloat = 3.0 - self.borderMaskLayer.lineWidth = lineWidth - self.borderMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: 12.0).cgPath - } - } + private let duration: Double + private let cornerRadius: CGFloat + + private let backgroundView: AnimatedGradientView + + private let borderMaskView: UIImageView + private let borderBackgroundView: AnimatedGradientView + + private var params: Params? + + init(effectAlpha: CGFloat, borderAlpha: CGFloat, cornerRadius: CGFloat = 12.0, duration: Double) { + self.duration = duration + self.cornerRadius = cornerRadius - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) + self.backgroundView = AnimatedGradientView(effectAlpha: effectAlpha, duration: duration) - transition.setFrame(view: self.borderContainerView, frame: CGRect(origin: CGPoint(), size: size)) - transition.setFrame(view: self.borderGradientView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) + self.borderMaskView = UIImageView() + self.borderMaskView.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: nil, strokeColor: .white, strokeWidth: 2.0) + self.borderBackgroundView = AnimatedGradientView(effectAlpha: borderAlpha, duration: duration) + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + + self.borderBackgroundView.mask = self.borderMaskView + self.addSubview(self.borderBackgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat, transition: ComponentTransition) { + let params = Params(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth) + if self.params == params { + return + } + self.params = params - self.updateAnimations(size: size) + self.backgroundView.update(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth, transition: transition) + self.borderBackgroundView.update(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth, transition: transition) + transition.setFrame(view: self.borderMaskView, frame: CGRect(origin: CGPoint(), size: size)) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 8e1a2c12bed..db52366c584 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7097,20 +7097,19 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } } -private func calculateUseV2(context: AccountContext) -> Bool { - /*var useV2 = true +public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { + var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] { useV2 = false } - return useV2*/ - return false + return useV2 } public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { - let useV2 = calculateUseV2(context: accountContext) + let useV2 = shouldUseV2VideoChatImpl(context: accountContext) if useV2 { return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any } @@ -7120,7 +7119,7 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte } public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { - let useV2 = calculateUseV2(context: accountContext) + let useV2 = shouldUseV2VideoChatImpl(context: accountContext) if useV2 { return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift index 0edd0f7c02c..c644942dc0d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift @@ -5,12 +5,16 @@ import Display private final class VoiceChatMicrophoneNodeDrawingState: NSObject { let color: UIColor + let shadowColor: UIColor? + let shadowBlur: CGFloat let filled: Bool let transition: CGFloat let reverse: Bool - init(color: UIColor, filled: Bool, transition: CGFloat, reverse: Bool) { + init(color: UIColor, shadowColor: UIColor?, shadowBlur: CGFloat, filled: Bool, transition: CGFloat, reverse: Bool) { self.color = color + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur self.filled = filled self.transition = transition self.reverse = reverse @@ -24,11 +28,15 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { let muted: Bool let color: UIColor let filled: Bool + let shadowColor: UIColor? + let shadowBlur: CGFloat - init(muted: Bool, filled: Bool, color: UIColor) { + init(muted: Bool, filled: Bool, color: UIColor, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) { self.muted = muted self.filled = filled self.color = color + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } static func ==(lhs: State, rhs: State) -> Bool { @@ -41,6 +49,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { if lhs.filled != rhs.filled { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } } @@ -122,6 +136,8 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0 var color = self.state.color + var shadowColor = self.state.shadowColor + var shadowBlur = self.state.shadowBlur var reverse = false if let transitionContext = self.transitionContext { @@ -138,9 +154,17 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { if transitionContext.previousState.color.rgb != color.rgb { color = transitionContext.previousState.color.interpolateTo(color, fraction: t)! } + + if let previousShadowColor = transitionContext.previousState.shadowColor, let shadowColorValue = shadowColor, previousShadowColor.rgb != shadowColorValue.rgb { + shadowColor = previousShadowColor.interpolateTo(shadowColorValue, fraction: t)! + } + + if transitionContext.previousState.shadowBlur != shadowBlur { + shadowBlur = transitionContext.previousState.shadowBlur * (1.0 - t) + shadowBlur * t + } } - return VoiceChatMicrophoneNodeDrawingState(color: color, filled: self.state.filled, transition: transitionFraction, reverse: reverse) + return VoiceChatMicrophoneNodeDrawingState(color: color, shadowColor: shadowColor, shadowBlur: shadowBlur, filled: self.state.filled, transition: transitionFraction, reverse: reverse) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -155,9 +179,18 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { guard let parameters = parameters as? VoiceChatMicrophoneNodeDrawingState else { return } + + var bounds = bounds + bounds = bounds.insetBy(dx: parameters.shadowBlur, dy: parameters.shadowBlur) + + context.translateBy(x: bounds.minX, y: bounds.minY) context.setFillColor(parameters.color.cgColor) + if let shadowColor = parameters.shadowColor, parameters.shadowBlur != 0.0 { + context.setShadow(offset: CGSize(), blur: parameters.shadowBlur, color: shadowColor.cgColor) + } + var clearLineWidth: CGFloat = 2.0 var lineWidth: CGFloat = 1.0 + UIScreenPixel if bounds.size.width > 36.0 { diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index fbf747c2cfb..8b304e83119 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -212,6 +212,7 @@ struct AccountMutableState { var namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]] var updatedOutgoingUniqueMessageIds: [Int64: Int32] var storedStories: [StoryId: UpdatesStoredStory] + var sentScheduledMessageIds: Set var resetForumTopicLists: [PeerId: StateResetForumTopics] = [:] @@ -231,7 +232,7 @@ struct AccountMutableState { var authorizationListUpdated: Bool = false - init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { + init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], initialSentScheduledMessageIds: Set) { self.initialState = initialState self.state = initialState.state self.peers = initialPeers @@ -240,6 +241,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = initialReferencedGeneralMessageIds self.storedMessages = initialStoredMessages self.storedStories = initialStoredStories + self.sentScheduledMessageIds = initialSentScheduledMessageIds self.readInboxMaxIds = initialReadInboxMaxIds self.channelStates = initialState.channelStates self.peerChatInfos = initialState.peerChatInfos @@ -249,7 +251,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], sentScheduledMessageIds: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -260,6 +262,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = referencedGeneralMessageIds self.storedMessages = storedMessages self.storedStories = storedStories + self.sentScheduledMessageIds = sentScheduledMessageIds self.peerChatInfos = peerChatInfos self.readInboxMaxIds = readInboxMaxIds self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp @@ -271,7 +274,7 @@ struct AccountMutableState { } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, sentScheduledMessageIds: self.sentScheduledMessageIds, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { @@ -282,6 +285,8 @@ struct AccountMutableState { self.storedStories[id] = story } + self.sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + for i in other.branchOperationIndex ..< other.operations.count { self.addOperation(other.operations[i]) } @@ -357,6 +362,10 @@ struct AccountMutableState { self.addOperation(.DeleteMessages(messageIds)) } + mutating func addSentScheduledMessageIds(_ messageIds: [MessageId]) { + self.sentScheduledMessageIds.formUnion(messageIds) + } + mutating func editMessage(_ id: MessageId, message: StoreMessage) { self.addOperation(.EditMessage(id, message)) } @@ -842,13 +851,15 @@ struct AccountReplayedFinalState { let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: Int64] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] + let sentScheduledMessageIds: Set } struct AccountFinalStateEvents { let addedIncomingMessageIds: [MessageId] let addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] - let wasScheduledMessageIds:[MessageId] + let wasScheduledMessageIds: [MessageId] let deletedMessageIds: [DeletedMessageId] + let sentScheduledMessageIds: Set let updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] let updatedWebpages: [MediaId: TelegramMediaWebpage] let updatedCalls: [Api.PhoneCall] @@ -873,10 +884,10 @@ struct AccountFinalStateEvents { let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set()) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -903,6 +914,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = updatedRevenueBalances self.updatedStarsBalance = updatedStarsBalance self.updatedStarsRevenueStatus = updatedStarsRevenueStatus + self.sentScheduledMessageIds = sentScheduledMessageIds } init(state: AccountReplayedFinalState) { @@ -932,6 +944,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = state.updatedRevenueBalances self.updatedStarsBalance = state.updatedStarsBalance self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus + self.sentScheduledMessageIds = state.sentScheduledMessageIds } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -961,6 +974,9 @@ struct AccountFinalStateEvents { let isPremiumUpdated = self.isPremiumUpdated || other.isPremiumUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs })) + var sentScheduledMessageIds = self.sentScheduledMessageIds + sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds) } } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 10ae195bcbd..7d2dbab4148 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -272,7 +272,7 @@ private var declaredEncodables: Void = { declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) }) declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) }) declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) }) - declareEncodable(WasScheduledMessageAttribute.self, f: { WasScheduledMessageAttribute(decoder: $0) }) + declareEncodable(PendingProcessingMessageAttribute.self, f: { PendingProcessingMessageAttribute(decoder: $0) }) declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) }) declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift index 440f522cfed..92ee56bc261 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift @@ -18,7 +18,7 @@ extension BotInfo { switch apiBotInfo { case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl): let photo: TelegramMediaImage? = descriptionPhoto.flatMap(telegramMediaImageFromApiPhoto) - let video: TelegramMediaFile? = descriptionDocument.flatMap(telegramMediaFileFromApiDocument) + let video: TelegramMediaFile? = descriptionDocument.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } var commands: [BotCommand] = [] if let apiCommands = apiCommands { commands = apiCommands.map { command in diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 65fa28f5136..40db07ab3f2 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -598,7 +598,7 @@ extension ChatContextResult { if let photo = photo, let parsedImage = telegramMediaImageFromApiPhoto(photo) { image = parsedImage } - if let document = document, let parsedFile = telegramMediaFileFromApiDocument(document) { + if let document = document, let parsedFile = telegramMediaFileFromApiDocument(document, altDocuments: []) { file = parsedFile } self = .internalReference(ChatContextResult.InternalReference(queryId: queryId, id: id, type: type, title: title, description: description, image: image, file: file, message: ChatContextResultMessage(apiMessage: sendMessage))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 09360ff164a..ea4bf38bc74 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -192,7 +192,7 @@ extension InstantPage { } } for file in files { - if let file = telegramMediaFileFromApiDocument(file), let id = file.id { + if let file = telegramMediaFileFromApiDocument(file, altDocuments: []), let id = file.id { media[id] = file } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 9c1e70ad3d1..581d20ecfb9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -89,6 +89,8 @@ extension ReplyMarkupButton { )) } self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId, maxQuantity: maxQuantity)) + case let .keyboardButtonCopy(text, payload): + self.init(title: text, titleWhenForwarded: nil, action: .copyText(payload: payload)) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 663cf616b52..1b650fa415e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -50,7 +50,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], var isAnimated = false inner: for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { refinedTag = .voiceOrInstantVideo } else { @@ -227,7 +227,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -254,7 +254,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } case let .messageActionRequestedPeer(_, peers): result.append(contentsOf: peers.map(\.peerId)) - case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _): + case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _, _): if let boostPeer = boostPeer { result.append(boostPeer.peerId) } @@ -350,9 +350,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) return (mediaMap, nil, nil, nil, nil) - case let .messageMediaDocument(flags, document, _, ttlSeconds): + case let .messageMediaDocument(flags, document, altDocuments, ttlSeconds): if let document = document { - if let mediaFile = telegramMediaFileFromApiDocument(document) { + if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { @@ -655,6 +655,12 @@ extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck): + var attributes: [MessageAttribute] = [] + + if (flags2 & (1 << 4)) != 0 { + attributes.append(PendingProcessingMessageAttribute(approximateCompletionTime: date)) + } + let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId var namespace = namespace @@ -676,8 +682,6 @@ extension StoreMessage { authorId = resolvedFromId } - var attributes: [MessageAttribute] = [] - var threadId: Int64? if let replyTo = replyTo { var threadMessageId: MessageId? @@ -725,7 +729,7 @@ extension StoreMessage { threadId = Int64(threadIdValue.id) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: namespace, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) } if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) @@ -867,7 +871,7 @@ extension StoreMessage { attributes.append(InlineBusinessBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBusinessBotId)), title: nil)) } - if namespace != Namespaces.Message.ScheduledCloud && namespace != Namespaces.Message.QuickReplyCloud { + if !Namespaces.Message.allNonRegular.contains(namespace) { if let views = views { attributes.append(ViewCountMessageAttribute(count: Int(views))) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index f1849166efb..de186a7a372 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -100,8 +100,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .joinedByRequest) case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text): return TelegramMediaAction(action: .webViewData(text)) - case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId): return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId)) case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): @@ -133,8 +143,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe } else { return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0)) } - case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiveawayLaunch(_, stars): return TelegramMediaAction(action: .giveawayLaunched(stars: stars)) case let .messageActionGiveawayResults(flags, winners, unclaimed): @@ -150,6 +170,21 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) + case let .messageActionStarGift(flags, apiGift, message, convertStars): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + guard let gift = StarGift(apiStarGift: apiGift) else { + return nil + } + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0)) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 611db7cedc4..76b2a46ea6e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -6,7 +6,7 @@ import TelegramApi func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> PixelDimensions? { for attribute in attributes { switch attribute { - case let .Video(_, size, _, _, _): + case let .Video(_, size, _, _, _, _): return size case let .ImageSize(size): return size @@ -20,7 +20,7 @@ func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> func durationForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> Double? { for attribute in attributes { switch attribute { - case let .Video(duration, _, _, _, _): + case let .Video(duration, _, _, _, _, _): return duration case let .Audio(_, duration, _, _, _): return Double(duration) @@ -99,7 +99,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt result.append(.ImageSize(size: PixelDimensions(width: w, height: h))) case .documentAttributeAnimated: result.append(.Animated) - case let .documentAttributeVideo(flags, duration, w, h, preloadSize, videoStart): + case let .documentAttributeVideo(flags, duration, w, h, preloadSize, videoStart, videoCodec): var videoFlags = TelegramMediaVideoFlags() if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) @@ -110,7 +110,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt if (flags & (1 << 3)) != 0 { videoFlags.insert(.isSilent) } - result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart)) + result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart, videoCodec: videoCodec)) case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() @@ -158,7 +158,7 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32, return (immediateThumbnailData, representations) } -func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMediaFile? { +func telegramMediaFileFromApiDocument(_ document: Api.Document, altDocuments: [Api.Document]?) -> TelegramMediaFile? { switch document { case let .document(_, id, accessHash, fileReference, _, mimeType, size, thumbs, videoThumbs, dcId, attributes): var parsedAttributes = telegramMediaFileAttributesFromApiAttributes(attributes) @@ -182,8 +182,13 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia } } } + + var alternativeRepresentations: [Media] = [] + if let altDocuments { + alternativeRepresentations = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } + } - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes, alternativeRepresentations: alternativeRepresentations) case .documentEmpty: return nil } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift index 31d988351b5..e4365a2a0e0 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift @@ -9,7 +9,7 @@ extension TelegramMediaGame { case let .game(_, id, accessHash, shortName, title, description, photo, document): var file: TelegramMediaFile? if let document = document { - file = telegramMediaFileFromApiDocument(document) + file = telegramMediaFileFromApiDocument(document, altDocuments: []) } self.init(gameId: id, accessHash: accessHash, name: shortName, title: title, description: description, image: telegramMediaImageFromApiPhoto(photo), file: file) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index b5c63bcf8ba..a27d6773f87 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -9,7 +9,7 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa case let .webPageAttributeTheme(_, documents, settings): var files: [TelegramMediaFile] = [] if let documents = documents { - files = documents.compactMap { telegramMediaFileFromApiDocument($0) } + files = documents.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } } return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) case let .webPageAttributeStickerSet(apiFlags, stickers): @@ -21,7 +21,7 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa flags.insert(.isTemplate) } var files: [TelegramMediaFile] = [] - files = stickers.compactMap { telegramMediaFileFromApiDocument($0) } + files = stickers.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } return .stickerPack(TelegramMediaWebpageStickerPackAttribute(flags: flags, files: files)) case .webPageAttributeStory: return nil @@ -50,7 +50,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage) -> TelegramMedia } var file: TelegramMediaFile? if let document = document { - file = telegramMediaFileFromApiDocument(document) + file = telegramMediaFileFromApiDocument(document, altDocuments: []) } var story: TelegramMediaStory? var webpageAttributes: [TelegramMediaWebpageAttribute] = [] diff --git a/submodules/TelegramCore/Sources/ApiUtils/Theme.swift b/submodules/TelegramCore/Sources/ApiUtils/Theme.swift index 38f4fb14213..01feb7b6b10 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/Theme.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/Theme.swift @@ -8,7 +8,7 @@ extension TelegramTheme { convenience init(apiTheme: Api.Theme) { switch apiTheme { case let .theme(flags, id, accessHash, slug, title, document, settings, emoticon, installCount): - self.init(id: id, accessHash: accessHash, slug: slug, emoticon: emoticon, title: title, file: document.flatMap { telegramMediaFileFromApiDocument($0) }, settings: settings?.compactMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) + self.init(id: id, accessHash: accessHash, slug: slug, emoticon: emoticon, title: title, file: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, settings: settings?.compactMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift index 9bb1a7ae3f6..a7eebe3809a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift @@ -67,7 +67,7 @@ extension TelegramWallpaper { init(apiWallpaper: Api.WallPaper) { switch apiWallpaper { case let .wallPaper(id, flags, accessHash, slug, document, settings): - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { let wallpaperSettings: WallpaperSettings if let settings = settings { wallpaperSettings = WallpaperSettings(apiWallpaperSettings: settings) diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index e01f8261185..af95079f06b 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -13,12 +13,12 @@ extension MediaResourceReference { } } -final class TelegramCloudMediaResourceFetchInfo: MediaResourceFetchInfo { - let reference: MediaResourceReference - let preferBackgroundReferenceRevalidation: Bool - let continueInBackground: Bool +public final class TelegramCloudMediaResourceFetchInfo: MediaResourceFetchInfo { + public let reference: MediaResourceReference + public let preferBackgroundReferenceRevalidation: Bool + public let continueInBackground: Bool - init(reference: MediaResourceReference, preferBackgroundReferenceRevalidation: Bool, continueInBackground: Bool) { + public init(reference: MediaResourceReference, preferBackgroundReferenceRevalidation: Bool, continueInBackground: Bool) { self.reference = reference self.preferBackgroundReferenceRevalidation = preferBackgroundReferenceRevalidation self.continueInBackground = continueInBackground @@ -184,6 +184,12 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return representation.resource } } + + for alternativeRepresentation in file.alternativeRepresentations { + if let result = findMediaResource(media: alternativeRepresentation, previousMedia: previousMedia, resource: resource) { + return result + } + } } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { if let image = content.image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { @@ -254,6 +260,12 @@ func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> Telegra return representation.resource } } + + for alternativeRepresentation in file.alternativeRepresentations { + if let result = findMediaResourceById(media: alternativeRepresentation, resourceId: resourceId) { + return result + } + } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { if let image = content.image, let result = findMediaResourceById(media: image, resourceId: resourceId) { return result @@ -493,7 +505,7 @@ final class MediaReferenceRevalidationContext { return .fail(.generic) } for document in result { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { return .single(file) } } @@ -956,9 +968,12 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n } if let updatedResource = findUpdatedMediaResource(media: media, previousMedia: nil, resource: resource) { return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) - } else if let alternativeMedia = item.alternativeMedia, let updatedResource = findUpdatedMediaResource(media: alternativeMedia, previousMedia: nil, resource: resource) { - return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) } else { + for alternativeMediaValue in item.alternativeMediaList { + if let updatedResource = findUpdatedMediaResource(media: alternativeMediaValue, previousMedia: nil, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + } return .fail(.generic) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 317cae8aa50..46906016845 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -205,7 +205,7 @@ func augmentMediaWithReference(_ mediaReference: AnyMediaReference) -> Media { private func convertForwardedMediaForSecretChat(_ media: Media) -> Media { if let file = media as? TelegramMediaFile { - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, videoThumbnails: file.videoThumbnails, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, videoThumbnails: file.videoThumbnails, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes, alternativeRepresentations: []) } else if let image = media as? TelegramMediaImage { return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) } else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 63df2625db3..8346d55cb06 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -703,7 +703,7 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF attributes.append(.documentAttributeSticker(flags: flags, alt: displayText, stickerset: stickerSet, maskCoords: inputMaskCoords)) case .HasLinkedStickers: attributes.append(.documentAttributeHasStickers) - case let .Video(duration, size, videoFlags, preloadSize, coverTime): + case let .Video(duration, size, videoFlags, preloadSize, coverTime, videoCodec): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= (1 << 0) @@ -720,7 +720,10 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF if let coverTime = coverTime, coverTime > 0.0 { flags |= (1 << 4) } - attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize, videoStartTs: coverTime)) + if videoCodec != nil { + flags |= (1 << 5) + } + attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize, videoStartTs: coverTime, videoCodec: videoCodec)) case let .Audio(isVoice, duration, title, performer, waveform): var flags: Int32 = 0 if isVoice { @@ -790,7 +793,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA } else { return .audio } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(TelegramMediaVideoFlags.instantRoundVideo) { return .voiceMessages } else { @@ -1065,8 +1068,8 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { - case let .messageMediaDocument(_, document, _, _): - if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { + case let .messageMediaDocument(_, document, altDocuments, _): + if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 var ttlSeconds: Int32? if let autoclearMessageAttribute = autoclearMessageAttribute { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift index c0f8c1c1090..fa1f8033cee 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift @@ -258,7 +258,8 @@ private final class PendingPeerMediaUploadManagerImpl { message: message, cacheReferenceKey: nil, result: result, - accountPeerId: accountPeerId + accountPeerId: accountPeerId, + pendingMessageEvent: { _ in } ) |> deliverOn(queue)).start(completed: { [weak self, weak context] in guard let strongSelf = self, let initialContext = context else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 9a0baa37cfd..fd9398aa56a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -524,7 +524,7 @@ private func sendUploadedMessageContent( |> switchToLatest } -public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?) -> Signal { +public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?, threadId: Int32? = nil) -> Signal { let content: Signal if let media = media { switch media { @@ -561,14 +561,14 @@ public func standaloneSendMessage(account: Account, peerId: PeerId, text: String case let .progress(progress): return .single(progress) case let .result(result): - let sendContent = sendMessageContent(account: account, peerId: peerId, attributes: attributes, content: result) |> map({ _ -> Float in return 1.0 }) + let sendContent = sendMessageContent(account: account, peerId: peerId, attributes: attributes, content: result, threadId: threadId) |> map({ _ -> Float in return 1.0 }) return .single(1.0) |> then(sendContent |> mapError { _ -> StandaloneSendMessageError in }) } } } -private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent) -> Signal { +private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent, threadId: Int32?) -> Signal { return account.postbox.transaction { transaction -> Signal in if peerId.namespace == Namespaces.Peer.SecretChat { return .complete() @@ -631,9 +631,12 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) } + } else if let threadId { + flags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) |> `catch` { _ -> Signal in return .complete() } @@ -649,6 +652,9 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) } + } else if let threadId { + flags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 0479620b2dd..2f991264127 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -162,9 +162,9 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, _, _): + case let .messageMediaDocument(_, document, altDocuments, _): if let document = document { - if let mediaFile = telegramMediaFileFromApiDocument(document) { + if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return .single(.result(.media(.standalone(media: mediaFile)))) } } @@ -194,7 +194,7 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P |> mapToSignal { result -> Signal in switch result { case let .encryptedFile(id, accessHash, size, dcId, _): - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: size, attributes: attributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: size, attributes: attributes, alternativeRepresentations: []) return .single(.result(.media(.standalone(media: media)))) case .encryptedFileEmpty: diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e5b49e557ca..9503219ab24 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -529,7 +529,7 @@ func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, a } } - let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp) + let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp, initialSentScheduledMessageIds: Set()) return state } @@ -1666,12 +1666,19 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.QuickReplyCloud) { updatedState.addQuickReplyMessages([message]) } - case let .updateDeleteScheduledMessages(peer, messages): + case let .updateDeleteScheduledMessages(_, peer, messages, sentMessages): var messageIds: [MessageId] = [] + var sentMessageIds: [MessageId] = [] for message in messages { messageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.ScheduledCloud, id: message)) } + if let sentMessages { + for message in sentMessages { + sentMessageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: message)) + } + } updatedState.deleteMessages(messageIds) + updatedState.addSentScheduledMessageIds(sentMessageIds) case let .updateDeleteQuickReplyMessages(_, messages): var messageIds: [MessageId] = [] for message in messages { @@ -2628,7 +2635,7 @@ func pollChannelOnce(accountPeerId: PeerId, postbox: Postbox, network: Network, peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -2685,7 +2692,7 @@ public func standalonePollChannelOnce(accountPeerId: PeerId, postbox: Postbox, n peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -3286,6 +3293,8 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) } currentAddMessages = nil + currentAddScheduledMessages = nil + currentAddQuickReplyMessages = nil result.append(operation) case let .UpdateState(state): updatedState = state @@ -4762,7 +4771,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -4796,7 +4805,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -4980,7 +4989,7 @@ func replayFinalState( } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys @@ -5340,5 +5349,29 @@ func replayFinalState( _internal_setStarsReactionDefaultToPrivate(isPrivate: updatedStarsReactionsAreAnonymousByDefault, transaction: transaction) } - return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus) + return AccountReplayedFinalState( + state: finalState, + addedIncomingMessageIds: addedIncomingMessageIds, + addedReactionEvents: addedReactionEvents, + wasScheduledMessageIds: wasScheduledMessageIds, + addedSecretMessageIds: addedSecretMessageIds, + deletedMessageIds: deletedMessageIds, + updatedTypingActivities: updatedTypingActivities, + updatedWebpages: updatedWebpages, + updatedCalls: updatedCalls, + addedCallSignalingData: addedCallSignalingData, + updatedGroupCallParticipants: updatedGroupCallParticipants, + storyUpdates: storyUpdates, + updatedPeersNearby: updatedPeersNearby, + isContactUpdates: isContactUpdates, + delayNotificatonsUntil: delayNotificatonsUntil, + updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, + updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, + updateConfig: updateConfig, + isPremiumUpdated: isPremiumUpdated, + updatedRevenueBalances: updatedRevenueBalances, + updatedStarsBalance: updatedStarsBalance, + updatedStarsRevenueStatus: updatedStarsRevenueStatus, + sentScheduledMessageIds: finalState.state.sentScheduledMessageIds + ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1ae6baf6ea4..1fc793db67f 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -63,6 +63,7 @@ public enum DeletedMessageId: Hashable { final class MessagesRemovedContext { private var messagesRemovedInteractively = Set() + private var messagesRemovedRemotely = Set() private var messagesRemovedInteractivelyLock = NSLock() func synchronouslyIsMessageDeletedInteractively(ids: [MessageId]) -> [EngineMessage.Id] { @@ -85,6 +86,26 @@ final class MessagesRemovedContext { return result } + func synchronouslyIsMessageDeletedRemotely(ids: [MessageId]) -> [EngineMessage.Id] { + var result: [EngineMessage.Id] = [] + + self.messagesRemovedInteractivelyLock.lock() + for id in ids { + let mappedId: DeletedMessageId + if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup { + mappedId = .global(id.id) + } else { + mappedId = .messageId(id) + } + if self.messagesRemovedRemotely.contains(mappedId) { + result.append(id) + } + } + self.messagesRemovedInteractivelyLock.unlock() + + return result + } + func addIsMessagesDeletedInteractively(ids: [DeletedMessageId]) { if ids.isEmpty { return @@ -94,6 +115,16 @@ final class MessagesRemovedContext { self.messagesRemovedInteractively.formUnion(ids) self.messagesRemovedInteractivelyLock.unlock() } + + func addIsMessagesDeletedRemotely(ids: [DeletedMessageId]) { + if ids.isEmpty { + return + } + + self.messagesRemovedInteractivelyLock.lock() + self.messagesRemovedRemotely.formUnion(ids) + self.messagesRemovedInteractivelyLock.unlock() + } } public final class AccountStateManager { @@ -297,6 +328,11 @@ public final class AccountStateManager { return self.forceSendPendingStarsReactionPipe.signal() } + fileprivate let sentScheduledMessageIdsPipe = ValuePipe>() + public var sentScheduledMessageIds: Signal, NoError> { + return self.sentScheduledMessageIdsPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -690,6 +726,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -768,11 +805,16 @@ public final class AccountStateManager { |> deliverOn(self.queue) |> mapToSignal { [weak self] state, invalidatedChannels, disableParallelChannelReset -> Signal<(difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool, resetState: Bool), NoError> in if let state = state, let authorizedState = state.state { - let flags: Int32 - let ptsTotalLimit: Int32? + var flags: Int32 = 0 + var ptsTotalLimit: Int32? - flags = 1 << 0 - ptsTotalLimit = 1000 + if !"".isEmpty { + flags |= 1 << 0 + ptsTotalLimit = 1000 + } + + flags = 0 + ptsTotalLimit = nil if let strongSelf = self { if !invalidatedChannels.isEmpty { @@ -830,6 +872,7 @@ public final class AccountStateManager { if let replayedState = replayedState { if !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } return (difference, replayedState, false, false) @@ -968,6 +1011,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1075,6 +1119,9 @@ public final class AccountStateManager { if !events.updatedIncomingThreadReadStates.isEmpty || !events.updatedOutgoingThreadReadStates.isEmpty { strongSelf.threadReadStateUpdatesPipe.putNext((events.updatedIncomingThreadReadStates, events.updatedOutgoingThreadReadStates)) } + if !events.sentScheduledMessageIds.isEmpty { + strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds) + } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) } @@ -1243,6 +1290,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -1291,6 +1339,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1383,6 +1432,7 @@ public final class AccountStateManager { if let replayedState = replayedState, !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1884,6 +1934,12 @@ public final class AccountStateManager { } } + public var sentScheduledMessageIds: Signal, NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + func forceSendPendingStarsReaction(messageId: MessageId) { self.impl.with { impl in impl.forceSendPendingStarsReactionPipe.putNext(messageId) @@ -2123,6 +2179,10 @@ public final class AccountStateManager { public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.messagesRemovedContext.synchronouslyIsMessageDeletedInteractively(ids: ids) } + + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.messagesRemovedContext.synchronouslyIsMessageDeletedRemotely(ids: ids) + } } func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index a0f2ee88a9d..bcc226f1d89 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -118,6 +118,7 @@ final class AccountTaskManager { tasks.add(managedDisabledChannelStatusIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconTopicEmoji, forceActualized: true).start()) tasks.add(managedPeerColorUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) + tasks.add(managedStarGiftsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 15d15fb5791..c5daa87694e 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1196,7 +1196,7 @@ public final class AccountViewTracker { switch result { case let .stickerSet(_, _, _, documents)?: for document in documents { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { if transaction.getMedia(file.fileId) != nil { let _ = transaction.updateMedia(file.fileId, update: file) } @@ -1472,7 +1472,7 @@ public final class AccountViewTracker { if i < slice.count { let value = result[i] transaction.updatePeerCachedData(peerIds: Set([slice[i].0]), update: { _, cachedData in - var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil, personalChannel: .unknown, botPreview: nil) + var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil, personalChannel: .unknown, botPreview: nil, starGiftsCount: nil) var flags = cachedData.flags if case .boolTrue = value { flags.insert(.premiumRequired) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index be44796999f..03302828e09 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -58,7 +58,7 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: } } -func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId) -> Signal { +func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId, pendingMessageEvent: @escaping (PeerPendingMessageDelivered) -> Void) -> Signal { return postbox.transaction { transaction -> Void in let messageId: Int32? var apiMessage: Api.Message? @@ -125,7 +125,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var sentStickers: [TelegramMediaFile] = [] var sentGifs: [TelegramMediaFile] = [] - if let updatedTimestamp = updatedTimestamp { + if let updatedTimestamp { transaction.offsetPendingMessagesTimestamps(lowerBound: message.id, excludeIds: Set([message.id]), timestamp: updatedTimestamp) } @@ -134,29 +134,18 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] transaction.updateMessage(message.id, update: { currentMessage in - let updatedId: MessageId - if let messageId = messageId { - var namespace: MessageId.Namespace = Namespaces.Message.Cloud - if Namespaces.Message.allQuickReply.contains(message.id.namespace) { - namespace = Namespaces.Message.QuickReplyCloud - } else if let updatedTimestamp = updatedTimestamp { - if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { - namespace = Namespaces.Message.ScheduledCloud - } - } else if Namespaces.Message.allScheduled.contains(message.id.namespace) { - namespace = Namespaces.Message.ScheduledCloud - } - updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId) - } else { - updatedId = currentMessage.id - } - let media: [Media] var attributes: [MessageAttribute] let text: String let forwardInfo: StoreMessageForwardInfo? let threadId: Int64? - if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { + + var namespace = Namespaces.Message.Cloud + if message.id.namespace == Namespaces.Message.ScheduledLocal { + namespace = Namespaces.Message.ScheduledCloud + } + + if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false, namespace: namespace) { media = updatedMessage.media attributes = updatedMessage.attributes text = updatedMessage.text @@ -195,12 +184,10 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedAttributes.append(MediaSpoilerMessageAttribute()) } - if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud { - for i in 0 ..< updatedAttributes.count { - if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { - updatedAttributes.remove(at: i) - break - } + for i in 0 ..< updatedAttributes.count { + if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { + updatedAttributes.remove(at: i) + break } } if Namespaces.Message.allQuickReply.contains(message.id.namespace) { @@ -225,6 +212,30 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes threadId = currentMessage.threadId } + let updatedId: MessageId + if let messageId = messageId { + var namespace: MessageId.Namespace = Namespaces.Message.Cloud + if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) { + namespace = Namespaces.Message.ScheduledCloud + } + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let updatedTimestamp = updatedTimestamp { + if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) { + namespace = Namespaces.Message.ScheduledCloud + } else { + if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { + namespace = Namespaces.Message.ScheduledCloud + } + } + } else if Namespaces.Message.allScheduled.contains(message.id.namespace) { + namespace = Namespaces.Message.ScheduledCloud + } + updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId) + } else { + updatedId = currentMessage.id + } + for attribute in currentMessage.attributes { if let attribute = attribute as? OutgoingMessageInfoAttribute { bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets @@ -358,12 +369,26 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes stateManager.addUpdates(result) stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: message.id.peerId)]) + + if let updatedMessage, case let .Id(id) = updatedMessage.id { + pendingMessageEvent(PeerPendingMessageDelivered( + id: id, + isSilent: updatedMessage.attributes.contains(where: { attribute in + if let attribute = attribute as? NotificationInfoMessageAttribute { + return attribute.flags.contains(.muted) + } else { + return false + } + }), + isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) + )) + } } } -func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { +func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates, pendingMessageEvents: @escaping ([PeerPendingMessageDelivered]) -> Void) -> Signal { guard !messages.isEmpty else { - return .complete() + return .single(Void()) } return postbox.transaction { transaction -> Void in @@ -372,8 +397,12 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage var namespace = Namespaces.Message.Cloud if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) { namespace = Namespaces.Message.QuickReplyCloud - } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { - namespace = Namespaces.Message.ScheduledCloud + } else if let message = messages.first, let apiMessage = result.messages.first { + if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + namespace = Namespaces.Message.ScheduledCloud + } else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud + } } var resultMessages: [MessageId: StoreMessage] = [:] @@ -538,6 +567,23 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage } stateManager.addUpdates(result) stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: messages[0].id.peerId)]) + + pendingMessageEvents(mapping.compactMap { message, _, updatedMessage -> PeerPendingMessageDelivered? in + guard case let .Id(id) = updatedMessage.id else { + return nil + } + return PeerPendingMessageDelivered( + id: id, + isSilent: updatedMessage.attributes.contains(where: { attribute in + if let attribute = attribute as? NotificationInfoMessageAttribute { + return attribute.flags.contains(.muted) + } else { + return false + } + }), + isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) + ) + }) } } diff --git a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift index 5842b06a9b5..8070509f5e5 100644 --- a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift +++ b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift @@ -216,7 +216,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ case let .availableEffects(hash, effects, documents): var files: [Int64: TelegramMediaFile] = [:] for document in documents { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { files[file.fileId.id] = file } } diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index d0a4f149a09..743c030556a 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -22,7 +22,8 @@ private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> Te immediateThumbnailData: nil, mimeType: isAnimatedSticker ? "application/x-tgsticker" : "image/webp", size: nil, - attributes: attributes + attributes: attributes, + alternativeRepresentations: [] ) } @@ -222,7 +223,6 @@ public final class AvailableReactions: Equatable, Codable { var reactions = reactions reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) - //TODO:release reactions.append(generateStarsReaction()) self.reactions = reactions } @@ -242,7 +242,6 @@ public final class AvailableReactions: Equatable, Codable { self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0 - //TODO:release var reactions = try container.decode([Reaction].self, forKey: .reactions) reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) reactions.append(generateStarsReaction()) @@ -261,23 +260,23 @@ private extension AvailableReactions.Reaction { convenience init?(apiReaction: Api.AvailableReaction) { switch apiReaction { case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation, aroundAnimation, centerIcon): - guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { + guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon, altDocuments: []) else { return nil } - guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation) else { + guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation, altDocuments: []) else { return nil } - guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation) else { + guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation, altDocuments: []) else { return nil } - guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation) else { + guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation, altDocuments: []) else { return nil } - guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else { + guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation, altDocuments: []) else { return nil } - let aroundAnimationFile = aroundAnimation.flatMap { telegramMediaFileFromApiDocument($0) } - let centerAnimationFile = centerIcon.flatMap { telegramMediaFileFromApiDocument($0) } + let aroundAnimationFile = aroundAnimation.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } + let centerAnimationFile = centerIcon.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } let isEnabled = (flags & (1 << 0)) == 0 let isPremium = (flags & (1 << 2)) != 0 self.init( diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 2eff15a2db6..30b75e85803 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -170,7 +170,7 @@ func resolveUnknownEmojiFiles(postbox: Postbox, source: FetchMessageHistoryHo for documentSet in documentSets { if let documentSet = documentSet { for document in documentSet { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { transaction.storeMediaIfNotPresent(media: file) } } diff --git a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift index 7c26efb038a..9f1018bef7f 100644 --- a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift @@ -65,7 +65,7 @@ private extension PremiumPromoConfiguration { var videos: [String: TelegramMediaFile] = [:] for (key, document) in zip(videoSections, videoFiles) { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { videos[key] = file } } diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 8e48ce953d6..8974f82500d 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -53,7 +53,7 @@ func managedRecentStickers(postbox: Postbox, network: Network, forceFetch: Bool case let .recentStickers(_, _, stickers, _): var items: [OrderedItemListEntry] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { if let entry = CodableEntry(RecentMediaItem(file)) { items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry)) } @@ -76,7 +76,7 @@ func managedRecentGifs(postbox: Postbox, network: Network, forceFetch: Bool = fa case let .savedGifs(_, gifs): var items: [OrderedItemListEntry] = [] for gif in gifs { - if let file = telegramMediaFileFromApiDocument(gif), let id = file.id { + if let file = telegramMediaFileFromApiDocument(gif, altDocuments: []), let id = file.id { if let entry = CodableEntry(RecentMediaItem(file)) { items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry)) } @@ -114,7 +114,7 @@ func managedSavedStickers(postbox: Postbox, network: Network, forceFetch: Bool = var items: [OrderedItemListEntry] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { var stringRepresentations: [String] = [] if let representations = fileStringRepresentations[id] { stringRepresentations = representations @@ -141,7 +141,7 @@ func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal Signal Signal PendingMessageFailureReason? } } +public struct PeerPendingMessageDelivered { + public var id: EngineMessage.Id + public var isSilent: Bool + public var isPendingProcessing: Bool + + public init(id: EngineMessage.Id, isSilent: Bool, isPendingProcessing: Bool) { + self.id = id + self.isSilent = isSilent + self.isPendingProcessing = isPendingProcessing + } +} + private final class PeerPendingMessagesSummaryContext { - var messageDeliveredSubscribers = Bag<((MessageId.Namespace, Bool)) -> Void>() + var messageDeliveredSubscribers = Bag<([PeerPendingMessageDelivered]) -> Void>() var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>() } @@ -270,29 +282,32 @@ public final class PendingMessageManager { } if !removedSecretMessageIds.isEmpty { - let _ = (self.postbox.transaction { transaction -> (Set, Bool) in - var silent = false - var peerIdsWithDeliveredMessages = Set() + let _ = (self.postbox.transaction { transaction -> [PeerId: [PeerPendingMessageDelivered]] in + var peerIdsWithDeliveredMessages: [PeerId: [PeerPendingMessageDelivered]] = [:] for id in removedSecretMessageIds { if let message = transaction.getMessage(id) { if message.isSentOrAcknowledged { - peerIdsWithDeliveredMessages.insert(id.peerId) + var silent = false if message.muted { silent = true } + if peerIdsWithDeliveredMessages[id.peerId] == nil { + peerIdsWithDeliveredMessages[id.peerId] = [] + } + peerIdsWithDeliveredMessages[id.peerId]?.append(PeerPendingMessageDelivered(id: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: id.id), isSilent: silent, isPendingProcessing: false)) } } } - return (peerIdsWithDeliveredMessages, silent) + return peerIdsWithDeliveredMessages } - |> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages, silent in + |> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages in guard let strongSelf = self else { return } - for peerId in peerIdsWithDeliveredMessages { + for (peerId, deliveredMessages) in peerIdsWithDeliveredMessages { if let context = strongSelf.peerSummaryContexts[peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((Namespaces.Message.Cloud, silent)) + subscriber(deliveredMessages) } } } @@ -1724,43 +1739,49 @@ public final class PendingMessageManager { } } - let silent = message.muted - var namespace = Namespaces.Message.Cloud if message.id.namespace == Namespaces.Message.QuickReplyLocal { - namespace = Namespaces.Message.QuickReplyCloud - } else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { - namespace = id.namespace - - if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { - self.correlationIdToSentMessageId.with { value in - value.mapping[correlationId] = id + } else if let apiMessage { + var isScheduled = false + if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + isScheduled = true + } + if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage { + if (flags2 & (1 << 4)) != 0 { + isScheduled = true + } + } + if let id = apiMessage.id(namespace: isScheduled ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { + self.correlationIdToSentMessageId.with { value in + value.mapping[correlationId] = id + } } } } - return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId) - |> afterDisposed { [weak self] in - if let strongSelf = self { - strongSelf.queue.async { + let queue = self.queue + return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId, pendingMessageEvent: { [weak self] pendingMessageDelivered in + queue.async { + if let strongSelf = self { if let context = strongSelf.peerSummaryContexts[message.id.peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((namespace, silent)) + subscriber([pendingMessageDelivered]) } } } } - } + }) } private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { - var silent = false var namespace = Namespaces.Message.Cloud - if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal { - namespace = Namespaces.Message.QuickReplyCloud - } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { - namespace = Namespaces.Message.ScheduledCloud - if message.muted { - silent = true + if let message = messages.first { + if message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + namespace = Namespaces.Message.ScheduledCloud + } else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud } } @@ -1777,22 +1798,22 @@ public final class PendingMessageManager { } } } + let queue = self.queue - return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result) - |> afterDisposed { [weak self] in - if let strongSelf = self { - strongSelf.queue.async { - if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId] { + return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result, pendingMessageEvents: { [weak self] pendingMessagesDelivered in + queue.async { + if let strongSelf = self { + if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId], !pendingMessagesDelivered.isEmpty { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((namespace, silent)) + subscriber(pendingMessagesDelivered) } } } } - } + }) } - public func deliveredMessageEvents(peerId: PeerId) -> Signal<(namespace: MessageId.Namespace, silent: Bool), NoError> { + public func deliveredMessageEvents(peerId: PeerId) -> Signal<[PeerPendingMessageDelivered], NoError> { return Signal { subscriber in let disposable = MetaDisposable() @@ -1805,8 +1826,8 @@ public final class PendingMessageManager { self.peerSummaryContexts[peerId] = summaryContext } - let index = summaryContext.messageDeliveredSubscribers.add({ namespace, silent in - subscriber.putNext((namespace, silent)) + let index = summaryContext.messageDeliveredSubscribers.add({ event in + subscriber.putNext(event) }) disposable.set(ActionDisposable { diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index ddf283a43d4..5d955da2ff7 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -610,7 +610,7 @@ extension TelegramMediaFileAttribute { } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(duration, w, h): - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil) } } } @@ -642,7 +642,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } @@ -674,7 +674,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } @@ -706,7 +706,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } @@ -793,7 +793,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)]) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaDocument(thumb, thumbW, thumbH, mimeType, size, key, iv, attributes, caption): @@ -813,7 +813,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaVideo(thumb, thumbW, thumbH, duration, mimeType, w, h, size, key, iv, caption): @@ -821,14 +821,14 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): @@ -861,7 +861,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 default: break } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) @@ -995,7 +995,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)]) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1016,12 +1016,12 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1040,14 +1040,14 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): @@ -1080,7 +1080,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 default: break } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) @@ -1274,7 +1274,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)]) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1295,12 +1295,12 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1319,14 +1319,14 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): @@ -1359,7 +1359,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 default: break } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) @@ -1475,7 +1475,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)]) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1496,12 +1496,12 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1520,14 +1520,14 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): @@ -1560,7 +1560,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 default: break } - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) + let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 46d4d1b6fe6..3450b1b4a75 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 187 + return 192 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift index 42c8bf5adb4..a4ac5b93601 100644 --- a/submodules/TelegramCore/Sources/State/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -291,7 +291,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect case let .stickerSetCovered(set, cover): let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } return (info, items) @@ -299,7 +299,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for cover in covers { - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } } @@ -339,7 +339,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for document in documents { - if let file = telegramMediaFileFromApiDocument(document), let id = file.id { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift index bed0706667c..b61049a1f43 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift @@ -178,7 +178,9 @@ private func validatePeerReadState(network: Network, postbox: Postbox, stateMana if case let .idBased(updatedMaxIncomingReadId, _, _, updatedCount, updatedMarkedUnread) = readState { if updatedCount != 0 || updatedMarkedUnread { if localMaxIncomingReadId > updatedMaxIncomingReadId { - return .retry + if !"".isEmpty { + return .retry + } } } } diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 79f03b83089..f6679915129 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -115,7 +115,11 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + var namespace = namespace + if (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud + } let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 90ad669f1c0..7fafc906bc1 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -145,7 +145,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee } return nil } |> mapToSignal { peer -> Signal in - guard let peer, let inputChannel = apiInputChannel(peer) else { + guard let peer, let inputPeer = apiInputPeer(peer) else { return .never() } @@ -154,7 +154,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee flags |= (1 << 1) } - return network.request(Api.functions.stats.getBroadcastRevenueStats(flags: flags, channel: inputChannel)) + return network.request(Api.functions.stats.getBroadcastRevenueStats(flags: flags, peer: inputPeer)) |> map { result -> RevenueStats? in return RevenueStats(apiRevenueStats: result, peerId: peerId) } @@ -354,13 +354,13 @@ private final class RevenueStatsTransactionsContextImpl { } |> mapToSignal { peer -> Signal<([RevenueStatsTransactionsContext.State.Transaction], Int32, Int32?), NoError> in if let peer { - guard let inputChannel = apiInputChannel(peer) else { + guard let inputPeer = apiInputPeer(peer) else { return .complete() } let offset = lastOffset ?? 0 let limit: Int32 = lastOffset == nil ? 25 : 50 - return account.network.request(Api.functions.stats.getBroadcastRevenueTransactions(channel: inputChannel, offset: offset, limit: limit), automaticFloodWait: false) + return account.network.request(Api.functions.stats.getBroadcastRevenueTransactions(peer: inputPeer, offset: offset, limit: limit), automaticFloodWait: false) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -502,7 +502,7 @@ public enum RequestRevenueWithdrawalError : Equatable { } func _internal_checkChannelRevenueWithdrawalAvailability(account: Account) -> Signal { - return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(channel: .inputChannelEmpty, password: .inputCheckPasswordEmpty)) + return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(peer: .inputPeerEmpty, password: .inputCheckPasswordEmpty)) |> mapError { error -> RequestRevenueWithdrawalError in if error.errorDescription == "PASSWORD_HASH_INVALID" { return .requestPassword @@ -530,7 +530,7 @@ func _internal_requestChannelRevenueWithdrawalUrl(account: Account, peerId: Peer } return account.postbox.transaction { transaction -> Signal in - guard let channel = transaction.getPeer(peerId) as? TelegramChannel, let inputChannel = apiInputChannel(channel) else { + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -555,7 +555,7 @@ func _internal_requestChannelRevenueWithdrawalUrl(account: Account, peerId: Peer return checkPassword |> mapToSignal { password -> Signal in - return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(channel: inputChannel, password: password), automaticFloodWait: false) + return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(peer: inputPeer, password: password), automaticFloodWait: false) |> mapError { error -> RequestRevenueWithdrawalError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index d734f0a14de..29dd3fbfcb1 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -321,7 +321,7 @@ private final class StoryStatsPublicForwardsContextImpl { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index f35daeb38a2..3e5616334fa 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -298,6 +298,7 @@ public struct CachedUserFlags: OptionSet { public static let readDatesPrivate = CachedUserFlags(rawValue: 1 << 2) public static let premiumRequired = CachedUserFlags(rawValue: 1 << 3) public static let adsEnabled = CachedUserFlags(rawValue: 1 << 4) + public static let canViewRevenue = CachedUserFlags(rawValue: 1 << 5) } public final class EditableBotInfo: PostboxCoding, Equatable { @@ -744,6 +745,7 @@ public final class CachedUserData: CachedPeerData { public let birthday: TelegramBirthday? public let personalChannel: CachedTelegramPersonalChannel public let botPreview: BotPreview? + public let starGiftsCount: Int32? public let peerIds: Set public let messageIds: Set @@ -782,9 +784,10 @@ public final class CachedUserData: CachedPeerData { self.birthday = nil self.personalChannel = .unknown self.botPreview = nil + self.starGiftsCount = nil } - public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?, personalChannel: CachedTelegramPersonalChannel, botPreview: BotPreview?) { + public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?, personalChannel: CachedTelegramPersonalChannel, botPreview: BotPreview?, starGiftsCount: Int32?) { self.about = about self.botInfo = botInfo self.editableBotInfo = editableBotInfo @@ -815,6 +818,7 @@ public final class CachedUserData: CachedPeerData { self.birthday = birthday self.personalChannel = personalChannel self.botPreview = botPreview + self.starGiftsCount = starGiftsCount self.peerIds = Set() @@ -880,6 +884,8 @@ public final class CachedUserData: CachedPeerData { self.personalChannel = decoder.decodeCodable(CachedTelegramPersonalChannel.self, forKey: "pchan") ?? .unknown self.botPreview = decoder.decodeCodable(BotPreview.self, forKey: "botPreview") + + self.starGiftsCount = decoder.decodeOptionalInt32ForKey("starGiftsCount") } public func encode(_ encoder: PostboxEncoder) { @@ -985,6 +991,12 @@ public final class CachedUserData: CachedPeerData { } else { encoder.encodeNil(forKey: "botPreview") } + + if let starGiftsCount = self.starGiftsCount { + encoder.encodeInt32(starGiftsCount, forKey: "starGiftsCount") + } else { + encoder.encodeNil(forKey: "starGiftsCount") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -1025,128 +1037,135 @@ public final class CachedUserData: CachedPeerData { if other.botPreview != self.botPreview { return false } + if other.starGiftsCount != self.starGiftsCount { + return false + } return other.about == self.about && other.botInfo == self.botInfo && other.editableBotInfo == self.editableBotInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.personalPhoto == other.personalPhoto && self.fallbackPhoto == other.fallbackPhoto && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable && self.flags == other.flags && self.wallpaper == other.wallpaper } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedEditableBotInfo(_ editableBotInfo: EditableBotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPhoto(_ photo: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPersonalPhoto(_ personalPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedFallbackPhoto(_ fallbackPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedFlags(_ flags: CachedUserFlags) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBusinessHours(_ businessHours: TelegramBusinessHours?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBusinessLocation(_ businessLocation: TelegramBusinessLocation?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedGreetingMessage(_ greetingMessage: TelegramBusinessGreetingMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedAwayMessage(_ awayMessage: TelegramBusinessAwayMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedConnectedBot(_ connectedBot: TelegramAccountConnectedBot?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBirthday(_ birthday: TelegramBirthday?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedPersonalChannel(_ personalChannel: TelegramPersonalChannel?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: .known(personalChannel), botPreview: self.botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: .known(personalChannel), botPreview: self.botPreview, starGiftsCount: self.starGiftsCount) } public func withUpdatedBotPreview(_ botPreview: BotPreview?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: botPreview) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: botPreview, starGiftsCount: self.starGiftsCount) + } + + public func withUpdatedStarGiftsCount(_ starGiftsCount: Int32?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview, starGiftsCount: starGiftsCount) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 64adab4a100..fb8c128a80b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -788,6 +788,35 @@ public enum MediaReference { } } + public func withMedia(_ media: T) -> MediaReference { + switch self { + case .standalone: + return .standalone(media: media) + case let .message(message, _): + return .message(message: message, media: media) + case let .webPage(webPage, _): + return .webPage(webPage: webPage, media: media) + case let .stickerPack(stickerPack, _): + return .stickerPack(stickerPack: stickerPack, media: media) + case .savedGif: + return .savedGif(media: media) + case .savedSticker: + return .savedSticker(media: media) + case .recentSticker: + return .recentSticker(media: media) + case let .avatarList(peer, _): + return .avatarList(peer: peer, media: media) + case let .attachBot(peer, _): + return .attachBot(peer: peer, media: media) + case .customEmoji: + return .customEmoji(media: media) + case let .story(peer, id, _): + return .story(peer: peer, id: id, media: media) + case let .starsTransaction(transaction, _): + return .starsTransaction(transaction: transaction, media: media) + } + } + public func resourceReference(_ resource: MediaResource) -> MediaResourceReference { return .media(media: self.abstract, resource: resource) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index ce6b31cf536..9109fdecc6e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -136,6 +136,7 @@ public struct Namespaces { public static let cachedRevenueStats: Int8 = 39 public static let recommendedApps: Int8 = 40 public static let starsReactionDefaultToPrivate: Int8 = 41 + public static let cachedPremiumGiftCodeOptions: Int8 = 42 } public struct UnorderedItemList { @@ -300,6 +301,7 @@ private enum PreferencesKeyValues: Int32 { case timezoneList = 38 case botBiometricsState = 39 case businessLinks = 40 + case starGifts = 41 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -524,6 +526,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.businessLinks.rawValue) return key } + + public static func starGifts() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.starGifts.rawValue) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift index acd70cce706..5b7a5302c18 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift @@ -232,6 +232,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case openUserProfile(peerId: PeerId) case openWebView(url: String, simple: Bool) case requestPeer(peerType: ReplyMarkupButtonRequestPeerType, buttonId: Int32, maxQuantity: Int32) + case copyText(payload: String) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -261,6 +262,8 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { self = .openWebView(url: decoder.decodeStringForKey("u", orElse: ""), simple: decoder.decodeInt32ForKey("s", orElse: 0) != 0) case 12: self = .requestPeer(peerType: decoder.decode(ReplyMarkupButtonRequestPeerType.self, forKey: "pt") ?? ReplyMarkupButtonRequestPeerType.user(ReplyMarkupButtonRequestPeerType.User(isBot: nil, isPremium: nil)), buttonId: decoder.decodeInt32ForKey("b", orElse: 0), maxQuantity: decoder.decodeInt32ForKey("q", orElse: 1)) + case 13: + self = .copyText(payload: decoder.decodeStringForKey("p", orElse: "")) default: self = .text } @@ -313,6 +316,9 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { encoder.encodeInt32(buttonId, forKey: "b") encoder.encode(peerType, forKey: "pt") encoder.encodeInt32(maxQuantity, forKey: "q") + case let .copyText(payload): + encoder.encodeInt32(13, forKey: "v") + encoder.encodeString(payload, forKey: "p") } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 8bee1013148..8fb86d73273 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -114,7 +114,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case setChatTheme(emoji: String) case joinedByRequest case webViewData(String) - case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?) case topicEdited(components: [ForumTopicEditComponent]) case suggestedProfilePhoto(image: TelegramMediaImage?) @@ -122,7 +122,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case requestedPeer(buttonId: Int32, peerIds: [PeerId]) case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool) case setSameChatWallpaper(wallpaper: TelegramWallpaper) - case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case giveawayLaunched(stars: Int64?) case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32, stars: Bool) @@ -130,6 +130,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) + case starGift(gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -199,7 +200,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 26: self = .webViewData(decoder.decodeStringForKey("t", orElse: "")) case 27: - self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 28: self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId")) case 29: @@ -229,7 +230,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 35: self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) case 36: - self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 37: self = .giveawayLaunched(stars: decoder.decodeOptionalInt64ForKey("stars")) case 38: @@ -250,6 +251,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { giveawayMessageId = MessageId(peerId: boostPeerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId) } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) + case 44: + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeInt64ForKey("convertStars", orElse: 0), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false)) default: self = .unknown } @@ -379,7 +382,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .webViewData(text): encoder.encodeInt32(26, forKey: "_rawValue") encoder.encodeString(text, forKey: "t") - case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount): + case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(27, forKey: "_rawValue") encoder.encodeString(currency, forKey: "currency") encoder.encodeInt64(amount, forKey: "amount") @@ -387,6 +390,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount { encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency") encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount") + } else { + encoder.encodeNil(forKey: "cryptoCurrency") + encoder.encodeNil(forKey: "cryptoAmount") + } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") } case let .topicCreated(title, iconColor, iconFileId): encoder.encodeInt32(28, forKey: "_rawValue") @@ -430,7 +443,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "atp") } - case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount): + case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(36, forKey: "_rawValue") encoder.encodeString(slug, forKey: "slug") encoder.encodeBool(fromGiveaway, forKey: "give") @@ -461,6 +474,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "cryptoAmount") } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") + } case let .giveawayLaunched(stars): encoder.encodeInt32(37, forKey: "_rawValue") if let stars = stars { @@ -525,6 +545,20 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted): + encoder.encodeInt32(44, forKey: "_rawValue") + encoder.encodeObject(gift, forKey: "gift") + encoder.encodeInt64(convertStars, forKey: "convertStars") + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") + } + encoder.encodeBool(nameHidden, forKey: "nameHidden") + encoder.encodeBool(savedToProfile, forKey: "savedToProfile") + encoder.encodeBool(converted, forKey: "converted") } } @@ -546,7 +580,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return peerIds case let .requestedPeer(_, peerIds): return peerIds - case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _): + case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _): return boostPeerId.flatMap { [$0] } ?? [] case let .paymentRefunded(peerId, _, _, _, _): return [peerId] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index aaddf2debb2..4e0c73441d5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -235,7 +235,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?) case ImageSize(size: PixelDimensions) case Animated - case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?, coverTime: Double?) + case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?, coverTime: Double?, videoCodec: String?) case Audio(isVoice: Bool, duration: Int, title: String?, performer: String?, waveform: Data?) case HasLinkedStickers case hintFileIsLarge @@ -262,7 +262,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { duration = Double(decoder.decodeInt32ForKey("du", orElse: 0)) } - self = .Video(duration: duration, size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)), preloadSize: decoder.decodeOptionalInt32ForKey("prs"), coverTime: decoder.decodeOptionalDoubleForKey("ct")) + self = .Video(duration: duration, size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)), preloadSize: decoder.decodeOptionalInt32ForKey("prs"), coverTime: decoder.decodeOptionalDoubleForKey("ct"), videoCodec: decoder.decodeOptionalStringForKey("vc")) case typeAudio: let waveformBuffer = decoder.decodeBytesForKeyNoCopy("wf") var waveform: Data? @@ -309,7 +309,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { encoder.encodeInt32(Int32(size.height), forKey: "h") case .Animated: encoder.encodeInt32(typeAnimated, forKey: "t") - case let .Video(duration, size, flags, preloadSize, coverTime): + case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec): encoder.encodeInt32(typeVideo, forKey: "t") encoder.encodeDouble(duration, forKey: "dur") encoder.encodeInt32(Int32(size.width), forKey: "w") @@ -325,6 +325,11 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "ct") } + if let videoCodec { + encoder.encodeString(videoCodec, forKey: "vc") + } else { + encoder.encodeNil(forKey: "vc") + } case let .Audio(isVoice, duration, title, performer, waveform): encoder.encodeInt32(typeAudio, forKey: "t") encoder.encodeInt32(isVoice ? 1 : 0, forKey: "iv") @@ -440,6 +445,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public let mimeType: String public let size: Int64? public let attributes: [TelegramMediaFileAttribute] + public let alternativeRepresentations: [Media] public let peerIds: [PeerId] = [] public var id: MediaId? { @@ -459,7 +465,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return result.isEmpty ? nil : result } - public init(fileId: MediaId, partialReference: PartialMediaReference?, resource: TelegramMediaResource, previewRepresentations: [TelegramMediaImageRepresentation], videoThumbnails: [TelegramMediaFile.VideoThumbnail], immediateThumbnailData: Data?, mimeType: String, size: Int64?, attributes: [TelegramMediaFileAttribute]) { + public init(fileId: MediaId, partialReference: PartialMediaReference?, resource: TelegramMediaResource, previewRepresentations: [TelegramMediaImageRepresentation], videoThumbnails: [TelegramMediaFile.VideoThumbnail], immediateThumbnailData: Data?, mimeType: String, size: Int64?, attributes: [TelegramMediaFileAttribute], alternativeRepresentations: [Media]) { self.fileId = fileId self.partialReference = partialReference self.resource = resource @@ -469,6 +475,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.mimeType = mimeType self.size = size self.attributes = attributes + self.alternativeRepresentations = alternativeRepresentations } public init(decoder: PostboxDecoder) { @@ -487,6 +494,13 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.size = nil } self.attributes = decoder.decodeObjectArrayForKey("at") + if let altMedia = try? decoder.decodeObjectArrayWithCustomDecoderForKey("arep", decoder: { d in + return d.decodeRootObject() as! Media + }) { + self.alternativeRepresentations = altMedia + } else { + self.alternativeRepresentations = [] + } } public func encode(_ encoder: PostboxEncoder) { @@ -513,6 +527,9 @@ public final class TelegramMediaFile: Media, Equatable, Codable { encoder.encodeNil(forKey: "s64") } encoder.encodeObjectArray(self.attributes, forKey: "at") + encoder.encodeObjectArrayWithEncoder(self.alternativeRepresentations, forKey: "arep", encoder: { v, e in + e.encodeRootObject(v) + }) } public required init(from decoder: Decoder) throws { @@ -531,6 +548,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.mimeType = object.mimeType self.size = object.size self.attributes = object.attributes + self.alternativeRepresentations = object.alternativeRepresentations } public func encode(to encoder: Encoder) throws { @@ -597,7 +615,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var isInstantVideo: Bool { for attribute in self.attributes { - if case .Video(_, _, let flags, _, _) = attribute { + if case .Video(_, _, let flags, _, _, _) = attribute { return flags.contains(.instantRoundVideo) } } @@ -606,7 +624,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var preloadSize: Int32? { for attribute in self.attributes { - if case .Video(_, _, _, let preloadSize, _) = attribute { + if case .Video(_, _, _, let preloadSize, _, _) = attribute { return preloadSize } } @@ -803,6 +821,10 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if !areMediaArraysEqual(self.alternativeRepresentations, other.alternativeRepresentations) { + return false + } + return true } @@ -849,27 +871,31 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if !areMediaArraysSemanticallyEqual(self.alternativeRepresentations, other.alternativeRepresentations) { + return false + } + return true } public func withUpdatedPartialReference(_ partialReference: PartialMediaReference?) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedResource(_ resource: TelegramMediaResource) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedSize(_ size: Int64?) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedPreviewRepresentations(_ previewRepresentations: [TelegramMediaImageRepresentation]) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedAttributes(_ attributes: [TelegramMediaFileAttribute]) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: attributes, alternativeRepresentations: self.alternativeRepresentations) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift index 5c0518226e5..0872978f071 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift @@ -179,7 +179,7 @@ public struct TelegramWallpaperNativeCodable: Codable { public enum TelegramWallpaper: Equatable { public static func emoticonWallpaper(emoticon: String) -> TelegramWallpaper { - return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(emoticon: emoticon))) + return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), settings: WallpaperSettings(emoticon: emoticon))) } public struct Gradient: Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift index 00696fa9fde..56bd0592412 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift @@ -1,13 +1,18 @@ import Foundation import Postbox -public class WasScheduledMessageAttribute: MessageAttribute { - public init() { +public class PendingProcessingMessageAttribute: MessageAttribute { + public let approximateCompletionTime: Int32 + + public init(approximateCompletionTime: Int32) { + self.approximateCompletionTime = approximateCompletionTime } required public init(decoder: PostboxDecoder) { + self.approximateCompletionTime = decoder.decodeInt32ForKey("et", orElse: 0) } public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.approximateCompletionTime, forKey: "et") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index fdac6337f09..f8ef324e336 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -730,6 +730,34 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct StarGiftsCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.starGiftsCount + } else { + return nil + } + } + } public struct LinkedDiscussionPeerId: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = EnginePeerCachedInfoItem @@ -1088,7 +1116,9 @@ public extension TelegramEngine.EngineData.Item { guard let view = view as? CachedPeerDataView else { preconditionFailure() } - if let cachedData = view.cachedPeerData as? CachedChannelData { + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.flags.contains(.canViewRevenue) + } else if let cachedData = view.cachedPeerData as? CachedChannelData { return cachedData.flags.contains(.canViewRevenue) } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index acc75aa7dbc..20c87296ea7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -420,14 +420,14 @@ private class AdMessagesHistoryContextImpl { } }) - let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(peerId).flatMap(apiInputChannel) + let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in + guard let inputPeer else { return .single((nil, [])) } - return account.network.request(Api.functions.channels.getSponsoredMessages(channel: inputChannel)) + return account.network.request(Api.functions.messages.getSponsoredMessages(peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -515,14 +515,14 @@ private class AdMessagesHistoryContextImpl { } func markAsSeen(opaqueId: Data) { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(self.peerId).flatMap(apiInputChannel) + let signal: Signal = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(self.peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { return .complete() } - return self.account.network.request(Api.functions.channels.viewSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) + return self.account.network.request(Api.functions.messages.viewSponsoredMessage(peer: inputPeer, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } @@ -531,8 +531,8 @@ private class AdMessagesHistoryContextImpl { self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId) } - func markAction(opaqueId: Data) { - _internal_markAdAction(account: self.account, peerId: self.peerId, opaqueId: opaqueId) + func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) { + _internal_markAdAction(account: self.account, peerId: self.peerId, opaqueId: opaqueId, media: media, fullscreen: fullscreen) } func remove(opaqueId: Data) { @@ -593,9 +593,9 @@ public class AdMessagesHistoryContext { } } - public func markAction(opaqueId: Data) { + public func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) { self.impl.with { impl in - impl.markAction(opaqueId: opaqueId) + impl.markAction(opaqueId: opaqueId, media: media, fullscreen: fullscreen) } } @@ -607,15 +607,22 @@ public class AdMessagesHistoryContext { } -func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: Data) { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(peerId).flatMap(apiInputChannel) +func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: Data, media: Bool, fullscreen: Bool) { + let signal: Signal = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { return .complete() } - return account.network.request(Api.functions.channels.clickSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) + var flags: Int32 = 0 + if media { + flags |= (1 << 0) + } + if fullscreen { + flags |= (1 << 1) + } + return account.network.request(Api.functions.messages.clickSponsoredMessage(flags: flags, peer: inputPeer, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 0123ff375c3..d353439b2ce 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -309,7 +309,7 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n for icon in botIcons { switch icon { case let .attachMenuBotIcon(_, name, icon, _): - if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon) { + if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) { icons[iconName] = icon } } @@ -544,7 +544,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network for icon in botIcons { switch icon { case let .attachMenuBotIcon(_, name, icon, _): - if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon) { + if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) { icons[iconName] = icon } } @@ -755,7 +755,7 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal if (botAppFlags & (1 << 2)) != 0 { appFlags.insert(.hasSettings) } - return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: appFlags)) + return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: appFlags)) case .botAppNotModified: return .complete() } @@ -770,7 +770,7 @@ extension BotApp { convenience init?(apiBotApp: Api.BotApp) { switch apiBotApp { case let .botApp(_, id, accessHash, shortName, title, description, photo, document, hash): - self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: []) + self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: []) case .botAppNotModified: return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 622ddeac154..dac0e5b6dbe 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -10,9 +10,9 @@ private let botWebViewPlatform = "macos" private let botWebViewPlatform = "ios" #endif -public enum RequestSimpleWebViewSource { +public enum RequestSimpleWebViewSource : Equatable { case generic - case inline + case inline(startParam: String?) case settings } @@ -30,8 +30,12 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = serializedThemeParams { flags |= (1 << 0) } + + var startParam: String? = nil + switch source { - case .inline: + case let .inline(_startParam): + startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) @@ -41,7 +45,7 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = url { flags |= (1 << 3) } - return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } @@ -77,15 +81,18 @@ func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: Pee if let _ = serializedThemeParams { flags |= (1 << 0) } + var startParam: String? = nil + switch source { - case .inline: + case let .inline(_startParam): + startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) default: break } - return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 523354bb0fc..efcd222dd56 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -530,7 +530,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -575,7 +575,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -615,7 +615,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -727,7 +727,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 019afadf9e2..c93f33e990f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -118,7 +118,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: if let dimensions = externalReference.content?.dimensions { fileAttributes.append(.ImageSize(size: dimensions)) if externalReference.type == "gif" { - fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } } @@ -136,7 +136,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: resource = EmptyMediaResource() } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes, alternativeRepresentations: []) return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } else { return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 989ff1ddd8d..f282a1ad39d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -742,7 +742,7 @@ extension TelegramBusinessIntro { convenience init(apiBusinessIntro: Api.BusinessIntro) { switch apiBusinessIntro { case let .businessIntro(_, title, description, sticker): - self.init(title: title, text: description, stickerFile: sticker.flatMap(telegramMediaFileFromApiDocument)) + self.init(title: title, text: description, stickerFile: sticker.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift index ae07cda8daa..61fb824b75a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift @@ -21,10 +21,10 @@ public enum ReportAdMessageError { func _internal_reportAdMessage(account: Account, peerId: EnginePeer.Id, opaqueId: Data, option: Data?) -> Signal { return account.postbox.transaction { transaction -> Signal in - guard let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) else { + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } - return account.network.request(Api.functions.channels.reportSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId), option: Buffer(data: option))) + return account.network.request(Api.functions.messages.reportSponsoredMessage(peer: inputPeer, randomId: Buffer(data: opaqueId), option: Buffer(data: option))) |> mapError { error -> ReportAdMessageError in if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { return .premiumRequired diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift new file mode 100644 index 00000000000..6d857d0551f --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift @@ -0,0 +1,82 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public enum ReportContentResult { + public struct Option: Equatable { + public let text: String + public let option: Data + } + + case options(title: String, options: [Option]) + case addComment(optional: Bool, option: Data) + case reported +} + +public enum ReportContentError { + case generic + case messageIdRequired +} + +public enum ReportContentSubject: Equatable { + case peer(EnginePeer.Id) + case messages([EngineMessage.Id]) + case stories(EnginePeer.Id, [Int32]) + + public var peerId: EnginePeer.Id { + switch self { + case let .peer(peerId): + return peerId + case let .messages(messageIds): + return messageIds.first!.peerId + case let .stories(peerId, _): + return peerId + } + } +} + +func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal { + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + let request: Signal + if case let .stories(_, ids) = subject { + request = account.network.request(Api.functions.stories.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? "")) + } else { + var ids: [Int32] = [] + if case let .messages(messageIds) = subject { + ids = messageIds.map { $0.id } + } + request = account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? "")) + } + + return request + |> mapError { error -> ReportContentError in + if error.errorDescription == "MESSAGE_ID_REQUIRED" { + return .messageIdRequired + } + return .generic + } + |> map { result -> ReportContentResult in + switch result { + case let .reportResultChooseOption(title, options): + return .options(title: title, options: options.map { + switch $0 { + case let .messageReportOption(text, option): + return ReportContentResult.Option(text: text, option: option.makeData()) + } + }) + case let .reportResultAddComment(flags, option): + return .addComment(optional: (flags & (1 << 0)) != 0, option: option.makeData()) + case .reportResultReported: + return .reported + } + } + } + |> castError(ReportContentError.self) + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index d26c4114268..2336fd44b7c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -245,6 +245,7 @@ public enum Stories { case expirationTimestamp case media case alternativeMedia + case alternativeMediaList case mediaAreas case text case entities @@ -268,7 +269,7 @@ public enum Stories { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: Media? - public let alternativeMedia: Media? + public let alternativeMediaList: [Media] public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -292,7 +293,7 @@ public enum Stories { timestamp: Int32, expirationTimestamp: Int32, media: Media?, - alternativeMedia: Media?, + alternativeMediaList: [Media], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], @@ -315,7 +316,7 @@ public enum Stories { self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media - self.alternativeMedia = alternativeMedia + self.alternativeMediaList = alternativeMediaList self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -348,10 +349,18 @@ public enum Stories { self.media = nil } - if let alternativeMediaData = try container.decodeIfPresent(Data.self, forKey: .alternativeMedia) { - self.alternativeMedia = PostboxDecoder(buffer: MemoryBuffer(data: alternativeMediaData)).decodeRootObject() as? Media + if let alternativeMediaListData = try container.decodeIfPresent([Data].self, forKey: .alternativeMediaList) { + self.alternativeMediaList = alternativeMediaListData.compactMap { data -> Media? in + return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media + } + } else if let alternativeMediaData = try container.decodeIfPresent(Data.self, forKey: .alternativeMedia) { + if let value = PostboxDecoder(buffer: MemoryBuffer(data: alternativeMediaData)).decodeRootObject() as? Media { + self.alternativeMediaList = [value] + } else { + self.alternativeMediaList = [] + } } else { - self.alternativeMedia = nil + self.alternativeMediaList = [] } self.mediaAreas = try container.decodeIfPresent([MediaArea].self, forKey: .mediaAreas) ?? [] @@ -388,12 +397,12 @@ public enum Stories { try container.encode(mediaData, forKey: .media) } - if let alternativeMedia = self.alternativeMedia { + let alternativeMediaListData = self.alternativeMediaList.map { alternativeMediaValue -> Data in let encoder = PostboxEncoder() - encoder.encodeRootObject(alternativeMedia) - let alternativeMediaData = encoder.makeData() - try container.encode(alternativeMediaData, forKey: .alternativeMedia) + encoder.encodeRootObject(alternativeMediaValue) + return encoder.makeData() } + try container.encode(alternativeMediaListData, forKey: .alternativeMediaList) try container.encode(self.mediaAreas, forKey: .mediaAreas) @@ -436,14 +445,8 @@ public enum Stories { } } - if let lhsAlternativeMedia = lhs.alternativeMedia, let rhsAlternativeMedia = rhs.alternativeMedia { - if !lhsAlternativeMedia.isEqual(to: rhsAlternativeMedia) { - return false - } - } else { - if (lhs.alternativeMedia == nil) != (rhs.alternativeMedia == nil) { - return false - } + if !areMediaArraysEqual(lhs.alternativeMediaList, rhs.alternativeMediaList) { + return false } if lhs.mediaAreas != rhs.mediaAreas { @@ -871,8 +874,9 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput mimeType: "video/mp4", size: nil, attributes: [ - TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil, coverTime: coverTime) - ] + TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil, coverTime: coverTime, videoCodec: nil) + ], + alternativeRepresentations: [] ) return fileMedia @@ -1209,7 +1213,7 @@ func _internal_uploadStoryImpl( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1617,7 +1621,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1649,7 +1653,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1846,7 +1850,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1877,7 +1881,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2093,11 +2097,11 @@ extension Stories.StoredItem { mergedForwardInfo = forwardFrom.flatMap(Stories.Item.ForwardInfo.init(apiForwardInfo:)) } - var parsedAlternativeMedia: Media? + var parsedAlternativeMedia: [Media] = [] switch media { - case let .messageMediaDocument(_, _, altDocument, _): - if let altDocument = altDocument { - parsedAlternativeMedia = telegramMediaFileFromApiDocument(altDocument) + case let .messageMediaDocument(_, _, altDocuments, _): + if let altDocuments { + parsedAlternativeMedia = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } } default: break @@ -2108,7 +2112,7 @@ extension Stories.StoredItem { timestamp: date, expirationTimestamp: expireDate, media: parsedMedia, - alternativeMedia: parsedAlternativeMedia, + alternativeMediaList: parsedAlternativeMedia, mediaAreas: mediaAreas?.compactMap(mediaAreaFromApiMediaArea) ?? [], text: caption ?? "", entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [], @@ -2173,7 +2177,7 @@ func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Ne timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2656,7 +2660,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2690,7 +2694,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index e22036f50ad..b6e2c1fd7fa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -67,7 +67,7 @@ public final class EngineStoryItem: Equatable { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: EngineMedia - public let alternativeMedia: EngineMedia? + public let alternativeMediaList: [EngineMedia] public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -87,12 +87,12 @@ public final class EngineStoryItem: Equatable { public let forwardInfo: ForwardInfo? public let author: EnginePeer? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMedia: EngineMedia?, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMediaList: [EngineMedia], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media - self.alternativeMedia = alternativeMedia + self.alternativeMediaList = alternativeMediaList self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -126,7 +126,7 @@ public final class EngineStoryItem: Equatable { if lhs.media != rhs.media { return false } - if lhs.alternativeMedia != rhs.alternativeMedia { + if lhs.alternativeMediaList != rhs.alternativeMediaList { return false } if lhs.mediaAreas != rhs.mediaAreas { @@ -205,7 +205,7 @@ public extension EngineStoryItem { timestamp: self.timestamp, expirationTimestamp: self.expirationTimestamp, media: self.media._asMedia(), - alternativeMedia: self.alternativeMedia?._asMedia(), + alternativeMediaList: self.alternativeMediaList.map { $0._asMedia() }, mediaAreas: self.mediaAreas, text: self.text, entities: self.entities, @@ -670,7 +670,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -839,7 +839,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1013,7 +1013,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1062,7 +1062,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1113,7 +1113,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1170,7 +1170,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1297,7 +1297,7 @@ public final class PeerStoryListContext: StoryListContext { public final class SearchStoryListContext: StoryListContext { public enum Source { - case hashtag(String) + case hashtag(EnginePeer.Id?, String) case mediaArea(MediaArea) } @@ -1368,21 +1368,33 @@ public final class SearchStoryListContext: StoryListContext { var searchHashtag: String? = nil var area: Api.MediaArea? = nil + var peer: Signal = .single(nil) var flags: Int32 = 0 switch source { - case let .hashtag(query): + case let .hashtag(peerId, query): if query.hasPrefix("#") { searchHashtag = String(query[query.index(after: query.startIndex)...]) } else { searchHashtag = query } flags |= (1 << 0) + + if let peerId { + peer = account.postbox.transaction { transaction in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + flags |= (1 << 2) + } case let .mediaArea(mediaArea): area = apiMediaAreasFromMediaAreas([mediaArea], transaction: nil).first flags |= (1 << 1) } - self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, offset: loadMoreToken, limit: Int32(limit))) + self.requestDisposable = (peer + |> castError(MTRpcError.self) + |> mapToSignal { inputPeer in + return account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, peer: inputPeer, offset: loadMoreToken, limit: Int32(limit))) + } |> map { result -> Api.stories.FoundStories? in return result } @@ -1416,7 +1428,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1565,7 +1577,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1637,7 +1649,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.storyItem.timestamp, expirationTimestamp: item.storyItem.expirationTimestamp, media: item.storyItem.media, - alternativeMedia: item.storyItem.alternativeMedia, + alternativeMediaList: item.storyItem.alternativeMediaList, mediaAreas: item.storyItem.mediaAreas, text: item.storyItem.text, entities: item.storyItem.entities, @@ -1755,7 +1767,7 @@ public final class PeerExpiringStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2211,7 +2223,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: 0, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2260,7 +2272,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2371,7 +2383,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2447,7 +2459,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: 0, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2510,7 +2522,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 9c2249435df..78293ee80cd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1249,8 +1249,8 @@ public extension TelegramEngine { } var selectedMedia: EngineMedia - if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { - selectedMedia = alternativeMedia + if let alternativeMediaValue = itemAndPeer.item.alternativeMediaList.first.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = EngineMedia(media) } @@ -1291,7 +1291,7 @@ public extension TelegramEngine { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1415,6 +1415,10 @@ public extension TelegramEngine { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.account.stateManager.synchronouslyIsMessageDeletedRemotely(ids: ids) + } + public func synchronouslyLookupCorrelationId(correlationId: Int64) -> EngineMessage.Id? { return self.account.pendingMessageManager.synchronouslyLookupCorrelationId(correlationId: correlationId) } @@ -1477,11 +1481,16 @@ public extension TelegramEngine { return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option) } + public func reportContent(subject: ReportContentSubject, option: Data?, message: String?) -> Signal { + return _internal_reportContent(account: self.account, subject: subject, option: option, message: message) + } + public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal { return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds) } - public func markAdAction(peerId: EnginePeer.Id, opaqueId: Data) { - _internal_markAdAction(account: self.account, peerId: peerId, opaqueId: opaqueId) + + public func markAdAction(peerId: EnginePeer.Id, opaqueId: Data, media: Bool, fullscreen: Bool) { + _internal_markAdAction(account: self.account, peerId: peerId, opaqueId: opaqueId, media: media, fullscreen: fullscreen) } public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 95a4ae2dfc6..f866267056f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -15,7 +15,7 @@ public enum AppStoreTransactionPurpose { case upgrade case restore case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) - case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64) + case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case stars(count: Int64, currency: String, amount: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) @@ -43,7 +43,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } - case let .giftCode(peerIds, boostPeerId, currency, amount): + case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities): return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in var flags: Int32 = 0 var apiBoostPeer: Api.InputPeer? @@ -59,8 +59,13 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran apiBoostPeer = apiPeer flags |= (1 << 0) } - - return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount) + + var message: Api.TextWithEntities? + if let text { + flags |= (1 << 1) + message = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities ?? [], associatedPeers: SimpleDictionary())) + } + return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message) } case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): return account.postbox.transaction { transaction -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 6adeaca770d..3fed1345364 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -8,11 +8,12 @@ public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) - case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) + case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption, text: String?, entities: [MessageTextEntity]?) case stars(option: StarsTopUpOption) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsChatSubscription(hash: String) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) + case starGift(hideName: Bool, peerId: EnginePeer.Id, giftId: Int64, text: String?, entities: [MessageTextEntity]?) } public struct BotPaymentInvoiceFields: OptionSet { @@ -130,7 +131,7 @@ public struct BotPaymentForm : Equatable { public let canSaveCredentials: Bool public let passwordMissing: Bool public let invoice: BotPaymentInvoice - public let paymentBotId: PeerId + public let paymentBotId: PeerId? public let providerId: PeerId? public let url: String? public let nativeProvider: BotPaymentNativeProvider? @@ -138,7 +139,7 @@ public struct BotPaymentForm : Equatable { public let savedCredentials: [BotPaymentSavedCredentials] public let additionalPaymentMethods: [BotPaymentMethod] - public init(id: Int64, canSaveCredentials: Bool, passwordMissing: Bool, invoice: BotPaymentInvoice, paymentBotId: PeerId, providerId: PeerId?, url: String?, nativeProvider: BotPaymentNativeProvider?, savedInfo: BotPaymentRequestedInfo?, savedCredentials: [BotPaymentSavedCredentials], additionalPaymentMethods: [BotPaymentMethod]) { + public init(id: Int64, canSaveCredentials: Bool, passwordMissing: Bool, invoice: BotPaymentInvoice, paymentBotId: PeerId?, providerId: PeerId?, url: String?, nativeProvider: BotPaymentNativeProvider?, savedInfo: BotPaymentRequestedInfo?, savedCredentials: [BotPaymentSavedCredentials], additionalPaymentMethods: [BotPaymentMethod]) { self.id = id self.canSaveCredentials = canSaveCredentials self.passwordMissing = passwordMissing @@ -282,7 +283,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) - case let .giftCode(users, currency, amount, option): + case let .giftCode(users, currency, amount, option, text, entities): var inputUsers: [Api.InputUser] = [] if !users.isEmpty { for peerId in users { @@ -292,7 +293,14 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv } } - let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount) + var inputPurposeFlags: Int32 = 0 + var textWithEntities: Api.TextWithEntities? + if let text, let entities { + inputPurposeFlags |= (1 << 1) + textWithEntities = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())) + } + + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: inputPurposeFlags, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: textWithEntities) var flags: Int32 = 0 if let _ = option.storeProductId { @@ -345,6 +353,20 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv flags |= (1 << 4) } return .inputInvoiceStars(purpose: .inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users)) + case let .starGift(hideName, peerId, giftId, text, entities): + guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else { + return nil + } + var flags: Int32 = 0 + if hideName { + flags |= (1 << 0) + } + var message: Api.TextWithEntities? + if let text, !text.isEmpty { + flags |= (1 << 1) + message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) + } + return .inputInvoiceStarGift(flags: flags, userId: inputUser, giftId: giftId, message: message) } } @@ -382,6 +404,9 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source case let .paymentFormStars(_, _, _, title, description, photo, invoice, _): let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) + case let .paymentFormStarGift(_, invoice): + let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) + return TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) } } |> mapError { _ -> BotPaymentFormRequestError in } @@ -452,6 +477,10 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) return BotPaymentForm(id: id, canSaveCredentials: false, passwordMissing: false, invoice: parsedInvoice, paymentBotId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: []) + + case let .paymentFormStarGift(id, invoice): + let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) + return BotPaymentForm(id: id, canSaveCredentials: false, passwordMissing: false, invoice: parsedInvoice, paymentBotId: nil, providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: []) } } |> mapError { _ -> BotPaymentFormRequestError in } @@ -566,6 +595,7 @@ public enum SendBotPaymentFormError { case precheckoutFailed case paymentFailed case alreadyPaid + case starGiftOutOfStock } public enum SendBotPaymentResult { @@ -660,7 +690,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa receiptMessageId = id } } - case .giftCode, .stars, .starsGift, .starsChatSubscription: + case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift: receiptMessageId = nil } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift index 65bff012cc4..6e82c5dd515 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -148,6 +148,79 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m } } +public final class CachedPremiumGiftCodeOptions: Codable { + public let options: [PremiumGiftCodeOption] + + public init(options: [PremiumGiftCodeOption]) { + self.options = options + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.options = try container.decode([PremiumGiftCodeOption].self, forKey: "t") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.options, forKey: "t") + } +} + +func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?, onlyCached: Bool = false) -> Signal<[PremiumGiftCodeOption], NoError> { + let cached = account.postbox.transaction { transaction -> Signal<[PremiumGiftCodeOption], NoError> in + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPremiumGiftCodeOptions, key: ValueBoxKey(length: 0)))?.get(CachedPremiumGiftCodeOptions.self) { + return .single(entry.options) + } + return .single([]) + } |> switchToLatest + + var flags: Int32 = 0 + if let _ = peerId { + flags |= 1 << 0 + } + let remote = account.postbox.transaction { transaction -> Peer? in + if let peerId = peerId { + return transaction.getPeer(peerId) + } + return nil + } + |> mapToSignal { peer in + let inputPeer = peer.flatMap(apiInputPeer) + return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in + return .single(nil) + } + |> mapToSignal { results -> Signal<[PremiumGiftCodeOption], NoError> in + let options = results?.map { PremiumGiftCodeOption(apiGiftCodeOption: $0) } ?? [] + return account.postbox.transaction { transaction -> [PremiumGiftCodeOption] in + if peerId == nil { + if let entry = CodableEntry(CachedPremiumGiftCodeOptions(options: options)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPremiumGiftCodeOptions, key: ValueBoxKey(length: 0)), entry: entry) + } + } + return options + } + } + } + if peerId == nil { + return cached + |> mapToSignal { cached in + if onlyCached && !cached.isEmpty { + return .single(cached) + } else { + return .single(cached) + |> then(remote) + } + } + } else { + return remote + } +} + + func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> { var flags: Int32 = 0 if let _ = peerId { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift new file mode 100644 index 00000000000..85a0117c97e --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -0,0 +1,539 @@ +import Foundation +import Postbox +import MtProtoKit +import SwiftSignalKit +import TelegramApi + +public final class StarGiftsList: Codable, Equatable { + public let items: [StarGift] + public let hashValue: Int32 + + public init(items: [StarGift], hashValue: Int32) { + self.items = items + self.hashValue = hashValue + } + + public static func ==(lhs: StarGiftsList, rhs: StarGiftsList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.hashValue != rhs.hashValue { + return false + } + return true + } +} + +public struct StarGift: Equatable, Codable, PostboxCoding { + enum CodingKeys: String, CodingKey { + case id + case file + case price + case convertStars + case availability + case soldOut + } + + public struct Availability: Equatable, Codable, PostboxCoding { + enum CodingKeys: String, CodingKey { + case remains + case total + } + + public let remains: Int32 + public let total: Int32 + + public init(remains: Int32, total: Int32) { + self.remains = remains + self.total = total + } + + public init(decoder: PostboxDecoder) { + self.remains = decoder.decodeInt32ForKey(CodingKeys.remains.rawValue, orElse: 0) + self.total = decoder.decodeInt32ForKey(CodingKeys.total.rawValue, orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.remains, forKey: CodingKeys.remains.rawValue) + encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue) + } + } + + public struct SoldOut: Equatable, Codable, PostboxCoding { + enum CodingKeys: String, CodingKey { + case firstSale + case lastSale + } + + public let firstSale: Int32 + public let lastSale: Int32 + + public init(firstSale: Int32, lastSale: Int32) { + self.firstSale = firstSale + self.lastSale = lastSale + } + + public init(decoder: PostboxDecoder) { + self.firstSale = decoder.decodeInt32ForKey(CodingKeys.firstSale.rawValue, orElse: 0) + self.lastSale = decoder.decodeInt32ForKey(CodingKeys.lastSale.rawValue, orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.firstSale, forKey: CodingKeys.firstSale.rawValue) + encoder.encodeInt32(self.lastSale, forKey: CodingKeys.lastSale.rawValue) + } + } + + public enum DecodingError: Error { + case generic + } + + public let id: Int64 + public let file: TelegramMediaFile + public let price: Int64 + public let convertStars: Int64 + public let availability: Availability? + public let soldOut: SoldOut? + + public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?) { + self.id = id + self.file = file + self.price = price + self.convertStars = convertStars + self.availability = availability + self.soldOut = soldOut + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(Int64.self, forKey: .id) + + if let fileData = try container.decodeIfPresent(Data.self, forKey: .file), let file = PostboxDecoder(buffer: MemoryBuffer(data: fileData)).decodeRootObject() as? TelegramMediaFile { + self.file = file + } else { + throw DecodingError.generic + } + + self.price = try container.decode(Int64.self, forKey: .price) + self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0 + self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability) + self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut) + } + + public init(decoder: PostboxDecoder) { + self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0) + self.file = decoder.decodeObjectForKey(CodingKeys.file.rawValue) as! TelegramMediaFile + self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0) + self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0) + self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { StarGift.Availability(decoder: $0) }) as? StarGift.Availability + self.soldOut = decoder.decodeObjectForKey(CodingKeys.soldOut.rawValue, decoder: { StarGift.SoldOut(decoder: $0) }) as? StarGift.SoldOut + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + + let encoder = PostboxEncoder() + encoder.encodeRootObject(self.file) + let fileData = encoder.makeData() + try container.encode(fileData, forKey: .file) + + try container.encode(self.price, forKey: .price) + try container.encode(self.convertStars, forKey: .convertStars) + try container.encodeIfPresent(self.availability, forKey: .availability) + try container.encodeIfPresent(self.soldOut, forKey: .soldOut) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue) + encoder.encodeObject(self.file, forKey: CodingKeys.file.rawValue) + encoder.encodeInt64(self.price, forKey: CodingKeys.price.rawValue) + encoder.encodeInt64(self.convertStars, forKey: CodingKeys.convertStars.rawValue) + if let availability = self.availability { + encoder.encodeObject(availability, forKey: CodingKeys.availability.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.availability.rawValue) + } + if let soldOut = self.soldOut { + encoder.encodeObject(soldOut, forKey: CodingKeys.soldOut.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.soldOut.rawValue) + } + } +} + +extension StarGift { + init?(apiStarGift: Api.StarGift) { + switch apiStarGift { + case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale): + var availability: Availability? + if let availabilityRemains, let availabilityTotal { + availability = Availability(remains: availabilityRemains, total: availabilityTotal) + } + var soldOut: SoldOut? + if let firstSale, let lastSale { + soldOut = SoldOut(firstSale: firstSale, lastSale: lastSale) + } + guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { + return nil + } + self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut) + } + } +} + +func _internal_cachedStarGifts(postbox: Postbox) -> Signal { + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.starGifts()])) + return postbox.combinedView(keys: [viewKey]) + |> map { views -> StarGiftsList? in + guard let view = views.views[viewKey] as? PreferencesView else { + return nil + } + guard let value = view.values[PreferencesKeys.starGifts()]?.get(StarGiftsList.self) else { + return nil + } + return value + } +} + +func _internal_keepCachedStarGiftsUpdated(postbox: Postbox, network: Network) -> Signal { + let updateSignal = _internal_cachedStarGifts(postbox: postbox) + |> take(1) + |> mapToSignal { list -> Signal in + return network.request(Api.functions.payments.getStarGifts(hash: 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return postbox.transaction { transaction in + switch result { + case let .starGifts(hash, gifts): + let starGiftsLists = StarGiftsList(items: gifts.compactMap { StarGift(apiStarGift: $0) }, hashValue: hash) + transaction.setPreferencesEntry(key: PreferencesKeys.starGifts(), value: PreferencesEntry(starGiftsLists)) + case .starGiftsNotModified: + break + } + } + |> ignoreValues + } + } + + return updateSignal +} + +func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal { + let poll = _internal_keepCachedStarGiftsUpdated(postbox: postbox, network: network) + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + +func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser else { + return .complete() + } + return account.network.request(Api.functions.payments.convertStarGift(userId: inputUser, msgId: messageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result in + if let result, case .boolTrue = result { + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedUserData, let starGiftsCount = cachedData.starGiftsCount { + var updatedData = cachedData + updatedData = updatedData.withUpdatedStarGiftsCount(max(0, starGiftsCount - 1)) + return updatedData + } else { + return cachedData + } + }) + } + } + return .complete() + } + |> ignoreValues + } +} + +func _internal_updateStarGiftAddedToProfile(account: Account, messageId: EngineMessage.Id, added: Bool) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser else { + return .complete() + } + var flags: Int32 = 0 + if !added { + flags |= (1 << 0) + } + return account.network.request(Api.functions.payments.saveStarGift(flags: flags, userId: inputUser, msgId: messageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> ignoreValues + } +} + +private var cachedAccountGifts: [EnginePeer.Id: [ProfileGiftsContext.State.StarGift]] = [:] + +private final class ProfileGiftsContextImpl { + private let queue: Queue + private let account: Account + private let peerId: PeerId + + private let disposable = MetaDisposable() + private let actionDisposable = MetaDisposable() + + private var gifts: [ProfileGiftsContext.State.StarGift] = [] + private var count: Int32? + private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) + + var _state: ProfileGiftsContext.State? + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + + init(queue: Queue, account: Account, peerId: EnginePeer.Id) { + self.queue = queue + self.account = account + self.peerId = peerId + + self.loadMore() + } + + deinit { + self.disposable.dispose() + self.actionDisposable.dispose() + } + + func loadMore() { + if case let .ready(true, initialNextOffset) = self.dataState { + if self.gifts.isEmpty, self.peerId == self.account.peerId, let cachedGifts = cachedAccountGifts[self.peerId] { + self.gifts = cachedGifts + } + + self.dataState = .loading + self.pushState() + + let peerId = self.peerId + let accountPeerId = self.account.peerId + let network = self.account.network + let postbox = self.account.postbox + let signal: Signal<([ProfileGiftsContext.State.StarGift], Int32, String?), NoError> = self.account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(peerId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal<([ProfileGiftsContext.State.StarGift], Int32, String?), NoError> in + guard let inputUser else { + return .single(([], 0, nil)) + } + return network.request(Api.functions.payments.getUserStarGifts(userId: inputUser, offset: initialNextOffset ?? "", limit: 32)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([ProfileGiftsContext.State.StarGift], Int32, String?), NoError> in + guard let result else { + return .single(([], 0, nil)) + } + return postbox.transaction { transaction -> ([ProfileGiftsContext.State.StarGift], Int32, String?) in + switch result { + case let .userStarGifts(_, count, apiGifts, nextOffset, users): + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + + let gifts = apiGifts.compactMap { ProfileGiftsContext.State.StarGift(apiUserStarGift: $0, transaction: transaction) } + return (gifts, count, nextOffset) + } + } + } + } + + self.disposable.set((signal + |> deliverOn(self.queue)).start(next: { [weak self] (gifts, count, nextOffset) in + guard let strongSelf = self else { + return + } + if initialNextOffset == nil, strongSelf.peerId == strongSelf.account.peerId { + cachedAccountGifts[strongSelf.peerId] = gifts + strongSelf.gifts = gifts + } else { + for gift in gifts { + strongSelf.gifts.append(gift) + } + } + + let updatedCount = max(Int32(strongSelf.gifts.count), count) + strongSelf.count = updatedCount + strongSelf.dataState = .ready(canLoadMore: count != 0 && updatedCount > strongSelf.gifts.count && nextOffset != nil, nextOffset: nextOffset) + strongSelf.pushState() + })) + } + } + + func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) { + self.actionDisposable.set( + _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added).startStrict() + ) + if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) { + self.gifts[index] = self.gifts[index].withSavedToProfile(added) + } + self.pushState() + } + + func convertStarGift(messageId: EngineMessage.Id) { + self.actionDisposable.set( + _internal_convertStarGift(account: self.account, messageId: messageId).startStrict() + ) + if let count = self.count { + self.count = max(0, count - 1) + } + self.gifts.removeAll(where: { $0.messageId == messageId }) + self.pushState() + } + + private func pushState() { + self._state = ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState) + self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState))) + } +} + +public final class ProfileGiftsContext { + public struct State: Equatable { + public struct StarGift: Equatable { + public let gift: TelegramCore.StarGift + public let fromPeer: EnginePeer? + public let date: Int32 + public let text: String? + public let entities: [MessageTextEntity]? + public let messageId: EngineMessage.Id? + public let nameHidden: Bool + public let savedToProfile: Bool + public let convertStars: Int64? + + public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift { + return StarGift( + gift: self.gift, + fromPeer: self.fromPeer, + date: self.date, + text: self.text, + entities: self.entities, + messageId: self.messageId, + nameHidden: self.nameHidden, + savedToProfile: savedToProfile, + convertStars: self.convertStars + ) + } + } + + public enum DataState: Equatable { + case loading + case ready(canLoadMore: Bool, nextOffset: String?) + } + + public var gifts: [ProfileGiftsContext.State.StarGift] + public var count: Int32? + public var dataState: ProfileGiftsContext.State.DataState + } + + private let queue: Queue = .mainQueue() + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + + return disposable + } + } + + public init(account: Account, peerId: EnginePeer.Id) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) { + self.impl.with { impl in + impl.updateStarGiftAddedToProfile(messageId: messageId, added: added) + } + } + + public func convertStarGift(messageId: EngineMessage.Id) { + self.impl.with { impl in + impl.convertStarGift(messageId: messageId) + } + } + + public var currentState: ProfileGiftsContext.State? { + var state: ProfileGiftsContext.State? + self.impl.syncWith { impl in + state = impl._state + } + return state + } +} + +private extension ProfileGiftsContext.State.StarGift { + init?(apiUserStarGift: Api.UserStarGift, transaction: Transaction) { + switch apiUserStarGift { + case let .userStarGift(flags, fromId, date, apiGift, message, msgId, convertStars): + guard let gift = StarGift(apiStarGift: apiGift) else { + return nil + } + self.gift = gift + if let fromPeerId = fromId.flatMap({ EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }) { + self.fromPeer = transaction.getPeer(fromPeerId).flatMap(EnginePeer.init) + } else { + self.fromPeer = nil + } + self.date = date + + if let message { + switch message { + case let .textWithEntities(text, entities): + self.text = text + self.entities = messageTextEntitiesFromApiEntities(entities) + } + } else { + self.text = nil + self.entities = nil + } + if let fromPeer = self.fromPeer, let msgId { + self.messageId = EngineMessage.Id(peerId: fromPeer.id, namespace: Namespaces.Message.Cloud, id: msgId) + } else { + self.messageId = nil + } + self.nameHidden = (flags & (1 << 0)) != 0 + self.savedToProfile = (flags & (1 << 5)) == 0 + self.convertStars = convertStars + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 4036abb1e67..48ea7ace5f3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -468,7 +468,7 @@ private final class StarsContextImpl { } var transactions = state.transactions if addTransaction { - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil), at: 0) } self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) @@ -490,7 +490,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipNumber): let parsedPeer: StarsContext.State.Transaction.Peer var paidMessageId: MessageId? var giveawayMessageId: MessageId? @@ -506,6 +506,8 @@ private extension StarsContext.State.Transaction { parsedPeer = .premiumBot case .starsTransactionPeerAds: parsedPeer = .ads + case .starsTransactionPeerAPI: + parsedPeer = .apiLimitExtension case .starsTransactionPeerUnsupported: parsedPeer = .unsupported case let .starsTransactionPeer(apiPeer): @@ -544,7 +546,7 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod) + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber) } } } @@ -595,6 +597,7 @@ public final class StarsContext { case fragment case premiumBot case ads + case apiLimitExtension case unsupported case peer(EnginePeer) } @@ -613,6 +616,8 @@ public final class StarsContext { public let giveawayMessageId: MessageId? public let media: [Media] public let subscriptionPeriod: Int32? + public let starGift: StarGift? + public let floodskipNumber: Int32? public init( flags: Flags, @@ -628,7 +633,9 @@ public final class StarsContext { paidMessageId: MessageId?, giveawayMessageId: MessageId?, media: [Media], - subscriptionPeriod: Int32? + subscriptionPeriod: Int32?, + starGift: StarGift?, + floodskipNumber: Int32? ) { self.flags = flags self.id = id @@ -644,6 +651,8 @@ public final class StarsContext { self.giveawayMessageId = giveawayMessageId self.media = media self.subscriptionPeriod = subscriptionPeriod + self.starGift = starGift + self.floodskipNumber = floodskipNumber } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -689,6 +698,12 @@ public final class StarsContext { if lhs.subscriptionPeriod != rhs.subscriptionPeriod { return false } + if lhs.starGift != rhs.starGift { + return false + } + if lhs.floodskipNumber != rhs.floodskipNumber { + return false + } return true } } @@ -863,10 +878,10 @@ public final class StarsContext { private final class StarsTransactionsContextImpl { private let account: Account private weak var starsContext: StarsContext? - private let peerId: EnginePeer.Id + fileprivate let peerId: EnginePeer.Id private let mode: StarsTransactionsContext.Mode - private var _state: StarsTransactionsContext.State + fileprivate var _state: StarsTransactionsContext.State private let _statePromise = Promise() var state: Signal { return self._statePromise.get() @@ -879,17 +894,24 @@ private final class StarsTransactionsContextImpl { init(account: Account, subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) { assert(Queue.mainQueue().isCurrent()) + + let currentTransactions: [StarsContext.State.Transaction] + self.account = account switch subject { + case let .starsTransactionsContext(transactionsContext): + self.peerId = transactionsContext.peerId + currentTransactions = transactionsContext.currentState?.transactions ?? [] case let .starsContext(starsContext): self.starsContext = starsContext self.peerId = starsContext.peerId + currentTransactions = starsContext.currentState?.transactions ?? [] case let .peer(peerId): self.peerId = peerId + currentTransactions = [] } self.mode = mode - let currentTransactions = self.starsContext?.currentState?.transactions ?? [] let initialTransactions: [StarsContext.State.Transaction] switch mode { case .all: @@ -903,7 +925,33 @@ private final class StarsTransactionsContextImpl { self._state = StarsTransactionsContext.State(transactions: initialTransactions, canLoadMore: true, isLoading: false) self._statePromise.set(.single(self._state)) - if let starsContext = self.starsContext { + if case let .starsTransactionsContext(transactionsContext) = subject { + self.stateDisposable = (transactionsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let currentTransactions = state.transactions + let filteredTransactions: [StarsContext.State.Transaction] + switch mode { + case .all: + filteredTransactions = currentTransactions + case .incoming: + filteredTransactions = currentTransactions.filter { $0.count > 0 } + case .outgoing: + filteredTransactions = currentTransactions.filter { $0.count < 0 } + } + + if !filteredTransactions.isEmpty && self._state.transactions.isEmpty && filteredTransactions != initialTransactions { + var updatedState = self._state + updatedState.transactions.removeAll(where: { $0.flags.contains(.isLocal) }) + for transaction in filteredTransactions.reversed() { + updatedState.transactions.insert(transaction, at: 0) + } + self.updateState(updatedState) + } + }) + } else if case let .starsContext(starsContext) = subject { self.stateDisposable = (starsContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self, let state else { @@ -1006,6 +1054,7 @@ public final class StarsTransactionsContext { fileprivate let impl: QueueLocalObject public enum Subject { + case starsTransactionsContext(StarsTransactionsContext) case starsContext(StarsContext) case peer(EnginePeer.Id) } @@ -1028,6 +1077,14 @@ public final class StarsTransactionsContext { } } + public var currentState: StarsTransactionsContext.State? { + var state: StarsTransactionsContext.State? + self.impl.syncWith { impl in + state = impl._state + } + return state + } + public func reload() { self.impl.with { $0.loadMore(reload: true) @@ -1045,6 +1102,14 @@ public final class StarsTransactionsContext { return StarsTransactionsContextImpl(account: account, subject: subject, mode: mode) }) } + + var peerId: EnginePeer.Id { + var peerId: EnginePeer.Id? + self.impl.syncWith { impl in + peerId = impl.peerId + } + return peerId! + } } private final class StarsSubscriptionsContextImpl { @@ -1229,10 +1294,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot guard let invoice = invoice else { return .fail(.generic) } - - let flags: Int32 = 0 - - return account.network.request(Api.functions.payments.sendStarsForm(flags: flags, formId: formId, invoice: invoice)) + return account.network.request(Api.functions.payments.sendStarsForm(formId: formId, invoice: invoice)) |> map { result -> SendBotPaymentResult in switch result { case let .paymentResult(updates): @@ -1288,6 +1350,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot receiptMessageId = nil case .starsChatSubscription: receiptMessageId = nil + case .starGift: + receiptMessageId = nil } } } @@ -1308,6 +1372,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot return .fail(.alreadyPaid) } else if error.errorDescription == "MEDIA_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { + return .fail(.starGiftOutOfStock) } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index a36471d4c8c..5a80d83372b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -54,8 +54,8 @@ public extension TelegramEngine { return _internal_applyPremiumGiftCode(account: self.account, slug: slug) } - public func premiumGiftCodeOptions(peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> { - return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId) + public func premiumGiftCodeOptions(peerId: EnginePeer.Id?, onlyCached: Bool = false) -> Signal<[PremiumGiftCodeOption], NoError> { + return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId, onlyCached: onlyCached) } public func premiumGiveawayInfo(peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal { @@ -101,5 +101,24 @@ public extension TelegramEngine { public func fulfillStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String) -> Signal { return _internal_fulfillStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId) } + + public func cachedStarGifts() -> Signal<[StarGift]?, NoError> { + return _internal_cachedStarGifts(postbox: self.account.postbox) + |> map { starGiftsList in + return starGiftsList?.items + } + } + + public func keepStarGiftsUpdated() -> Signal { + return _internal_keepCachedStarGiftsUpdated(postbox: self.account.postbox, network: self.account.network) + } + + public func convertStarGift(messageId: EngineMessage.Id) -> Signal { + return _internal_convertStarGift(account: self.account, messageId: messageId) + } + + public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) -> Signal { + return _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift index ce0e9ea56cd..2ea20ee4b9c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift @@ -104,7 +104,7 @@ public final class NotificationSoundList: Equatable, Codable { private extension NotificationSoundList.NotificationSound { convenience init?(apiDocument: Api.Document) { - guard let file = telegramMediaFileFromApiDocument(apiDocument) else { + guard let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []) else { return nil } self.init(file: file) @@ -313,7 +313,7 @@ func _internal_uploadNotificationSound(account: Account, title: String, data: Da return .generic } |> mapToSignal { result -> Signal in - guard let file = telegramMediaFileFromApiDocument(result) else { + guard let file = telegramMediaFileFromApiDocument(result, altDocuments: []) else { return .fail(.generic) } return account.postbox.transaction { transaction -> NotificationSoundList.NotificationSound in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 1f67bde1b43..5ee8026860a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -462,6 +462,10 @@ public extension EnginePeer { var addressName: String? { return self._asPeer().addressName } + + var usernames: [TelegramPeerUsername] { + return self._asPeer().usernames + } var indexName: EnginePeer.IndexName { return EnginePeer.IndexName(self._asPeer().indexName) @@ -520,6 +524,9 @@ public extension EnginePeer { if peer.id.isReplies { return true } + if peer.id.isVerificationCodes { + return true + } return (peer.id.namespace == Namespaces.Peer.CloudUser && (peer.id.id._internalGetInt64Value() == 777000 || peer.id.id._internalGetInt64Value() == 333000)) } return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 04ffbec5117..5d84874fbd3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -148,42 +148,44 @@ func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportR } func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { - return account.postbox.transaction { transaction -> Signal in - let groupedIds = messagesIdsGroupedByPeerId(messageIds) - let signals = groupedIds.values.compactMap { ids -> Signal? in - guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { - return nil - } - return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) - } - |> mapToSignal { _ -> Signal in - return .complete() - } - } - - return combineLatest(signals) - |> mapToSignal { _ -> Signal in - return .complete() - } - } |> switchToLatest + return .complete() +// return account.postbox.transaction { transaction -> Signal in +// let groupedIds = messagesIdsGroupedByPeerId(messageIds) +// let signals = groupedIds.values.compactMap { ids -> Signal? in +// guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { +// return nil +// } +// return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message)) +// |> `catch` { _ -> Signal in +// return .single(.boolFalse) +// } +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } +// +// return combineLatest(signals) +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } |> switchToLatest } func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal { - return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) - } - |> mapToSignal { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - } |> switchToLatest + return .complete() +// return account.postbox.transaction { transaction -> Signal in +// if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { +// return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message)) +// |> `catch` { _ -> Signal in +// return .single(.boolFalse) +// } +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } else { +// return .complete() +// } +// } |> switchToLatest } func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index f6099c2e550..aa07479d987 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } } } - + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var renderedMyPeers: [FoundPeer] = [] @@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } @@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 6f17d552baf..9121a3cbeef 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -258,7 +258,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullUser { - case let .userFull(_, _, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .userFull(_, _, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: userFullNotifySettings)]) } @@ -270,7 +270,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee previous = CachedUserData() } switch fullUser { - case let .userFull(userFullFlags, userFullFlags2, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, greetingMessage, awayMessage, businessIntro, birthday, personalChannelId, personalChannelMessage): + case let .userFull(userFullFlags, userFullFlags2, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, greetingMessage, awayMessage, businessIntro, birthday, personalChannelId, personalChannelMessage, starGiftsCount): let _ = stories let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 @@ -281,6 +281,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let premiumRequired = (userFullFlags & (1 << 29)) != 0 let translationsDisabled = (userFullFlags & (1 << 23)) != 0 let adsEnabled = (userFullFlags2 & (1 << 7)) != 0 + let canViewRevenue = (userFullFlags2 & (1 << 9)) != 0 var flags: CachedUserFlags = previous.flags if premiumRequired { @@ -303,6 +304,11 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { flags.remove(.adsEnabled) } + if canViewRevenue { + flags.insert(.canViewRevenue) + } else { + flags.remove(.canViewRevenue) + } let callsPrivate = (userFullFlags & (1 << 5)) != 0 let canPinMessages = (userFullFlags & (1 << 7)) != 0 @@ -414,6 +420,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedBirthday(mappedBirthday) .withUpdatedPersonalChannel(personalChannel) .withUpdatedBotPreview(botPreview) + .withUpdatedStarGiftsCount(starGiftsCount) } }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index 9ef7f49317d..1d87fe12af6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -582,6 +582,14 @@ func _internal_reindexCacheInBackground(account: Account, lowImpact: Bool) -> Si processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) } processResource(mediaMessages, file.resource, MediaResourceUserContentType(file: file)) + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeRepresentation = alternativeRepresentation as? TelegramMediaFile { + for representation in alternativeRepresentation.previewRepresentations { + processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) + } + processResource(mediaMessages, alternativeRepresentation.resource, MediaResourceUserContentType(file: file)) + } + } } else if let webpage = media as? TelegramMediaWebpage { if case let .Loaded(content) = webpage.content { if let image = content.image { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index 90644ade6b7..d21db933e63 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -62,10 +62,10 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, } } -func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal { +func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool, ignoreCache: Bool = false) -> Signal { return postbox.transaction { transaction -> CachedStickerPackResult? in if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) { - if local { + if local && !ignoreCache { return .result(info, items, true) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 6ba71719066..f8a8e133b18 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -80,15 +80,15 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) if let duration { - attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil)) + attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil, videoCodec: nil)) } attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, _, _): - if let document = document, let file = telegramMediaFileFromApiDocument(document), let uploadedResource = file.resource as? CloudDocumentMediaResource { + case let .messageMediaDocument(_, document, altDocuments, _): + if let document = document, let file = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let uploadedResource = file.resource as? CloudDocumentMediaResource { account.postbox.mediaBox.copyResourceData(from: resource.id, to: uploadedResource.id, synchronous: true) if let thumbnail, let previewRepresentation = file.previewRepresentations.first(where: { $0.dimensions == PixelDimensions(width: 320, height: 320) }) { account.postbox.mediaBox.copyResourceData(from: thumbnail.id, to: previewRepresentation.resource.id, synchronous: true) @@ -144,7 +144,7 @@ public extension ImportSticker { fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Animated) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } else if self.mimeType == "application/x-tgsticker" { fileAttributes.append(.FileName(fileName: "sticker.tgs")) fileAttributes.append(.Animated) @@ -159,7 +159,7 @@ public extension ImportSticker { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: self.mimeType, size: nil, attributes: fileAttributes), indexKeys: []) + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: self.mimeType, size: nil, attributes: fileAttributes, alternativeRepresentations: []), indexKeys: []) } } @@ -560,7 +560,7 @@ func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollect } let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var firstItem: StickerPackItem? - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) } infos.append((info, firstItem)) @@ -579,7 +579,7 @@ func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var firstItem: StickerPackItem? if let apiDocument = documents.first { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) } } @@ -642,7 +642,7 @@ private func parseStickerSetInfoAndItems(apiStickerSet: Api.messages.StickerSet) var items: [StickerPackItem] = [] for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index b89e44c713b..8e1ae1f68d7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -100,7 +100,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys @@ -123,7 +123,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti } } -func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool) -> Signal { +func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal { return _internal_cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized) |> map { result -> LoadedStickerPack in switch result { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index 70ff4f51902..66d4f6abf3a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -320,7 +320,7 @@ func _internal_searchStickers(account: Account, query: [String], scope: SearchSt var files: [TelegramMediaFile] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { files.append(file) if !currentItemIds.contains(id) { if file.isPremiumSticker { @@ -705,7 +705,7 @@ func _internal_searchStickers(account: Account, category: EmojiSearchCategories. var files: [TelegramMediaFile] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { files.append(file) if !currentItemIds.contains(id) { if file.isPremiumSticker { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 657fb8c1987..db64fe57ab4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -117,7 +117,7 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys @@ -199,7 +199,7 @@ func _internal_installStickerSetInteractively(account: Account, info: StickerPac var items:[StickerPackItem] = [] for apiDocument in apiDocuments { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: [])) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index a3cf7bfac56..2235132187d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -22,8 +22,8 @@ public extension TelegramEngine { return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote) } - public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool) -> Signal { - return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized) + public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal { + return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized, ignoreCache: ignoreCache) } public func randomGreetingSticker() -> Signal { @@ -361,7 +361,7 @@ public func _internal_resolveInlineStickers(postbox: Postbox, network: Network, for result in documentSets { if let result = result { for document in result { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { resultFiles[file.fileId.id] = file transaction.storeMediaIfNotPresent(media: file) } diff --git a/submodules/TelegramCore/Sources/Themes.swift b/submodules/TelegramCore/Sources/Themes.swift index 92d1992607a..9bb600e89a5 100644 --- a/submodules/TelegramCore/Sources/Themes.swift +++ b/submodules/TelegramCore/Sources/Themes.swift @@ -258,7 +258,7 @@ private func uploadTheme(account: Account, resource: MediaResource, thumbnailDat return account.network.request(Api.functions.account.uploadTheme(flags: flags, file: file, thumb: thumbnailFile, fileName: fileName, mimeType: mimeType)) |> mapError { _ in return UploadThemeError.generic } |> mapToSignal { document -> Signal in - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { return .single(.complete(file)) } else { return .fail(.generic) diff --git a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift index 6b71965fb9d..20a3890c134 100644 --- a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift @@ -112,7 +112,7 @@ public func parseMediaData(data: Data) -> Media? { if let photo = object as? Api.Photo { return telegramMediaImageFromApiPhoto(photo) } else if let document = object as? Api.Document { - return telegramMediaFileFromApiDocument(document) + return telegramMediaFileFromApiDocument(document, altDocuments: []) } } return nil diff --git a/submodules/TelegramCore/Sources/Utils/Log.swift b/submodules/TelegramCore/Sources/Utils/Log.swift index 4278b78c028..8936eb663cb 100644 --- a/submodules/TelegramCore/Sources/Utils/Log.swift +++ b/submodules/TelegramCore/Sources/Utils/Log.swift @@ -212,7 +212,35 @@ public final class Logger { return EmptyDisposable } } - +// MARK: Nicegram NCG-5828 call recording + public func collectLogs( + with basePath: String, + accountPathName: String? + ) -> Signal<[(String, String)], NoError> { + guard let accountPathName else { return .never() } + + return Signal { subscriber in + self.queue.async { + let logsPath: String = basePath + "/\(accountPathName)/calls" + var result: [(Date, String, String)] = [] + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + for url in files { + if url.lastPathComponent.hasSuffix(".log") { + if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate { + result.append((creationDate, url.lastPathComponent, url.path)) + } + } + } + } + result.sort(by: { $0.0 < $1.0 }) + subscriber.putNext(result.map { ($0.1, $0.2) }) + subscriber.putCompletion() + } + + return EmptyDisposable + } + } +// public func collectShortLog() -> Signal<[(Double, String)], NoError> { return Signal { subscriber in self.queue.async { diff --git a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift index 818111988f9..618da9b69ac 100644 --- a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift +++ b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift @@ -11,7 +11,7 @@ public enum MediaResourceStatsCategory { case voiceMessages } -final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { +public final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { public let statsCategory: MediaResourceStatsCategory public init(statsCategory: MediaResourceStatsCategory, userContentType: MediaResourceUserContentType?) { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index acc484da193..44b136b5eaf 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -579,6 +579,17 @@ public extension Message { } } +public extension Message { + var pendingProcessingAttribute: PendingProcessingMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? PendingProcessingMessageAttribute { + return attribute + } + } + return nil + } +} + public extension Message { func areReactionsTags(accountPeerId: PeerId) -> Bool { if self.id.peerId == accountPeerId { @@ -599,7 +610,7 @@ public func _internal_parseMediaAttachment(data: Data) -> Media? { if let photo = object as? Api.Photo { return telegramMediaImageFromApiPhoto(photo) } else if let file = object as? Api.Document { - return telegramMediaFileFromApiDocument(file) + return telegramMediaFileFromApiDocument(file, altDocuments: []) } else { return nil } diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 346ea6c676d..9ce6471df54 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -456,12 +456,24 @@ public func isServicePeer(_ peer: Peer) -> Bool { if peer.id.isReplies { return true } + if peer.id.isVerificationCodes { + return true + } return (peer.id.namespace == Namespaces.Peer.CloudUser && (peer.id.id._internalGetInt64Value() == 777000 || peer.id.id._internalGetInt64Value() == 333000)) } return false } public extension PeerId { + var isTelegramNotifications: Bool { + if self.namespace == Namespaces.Peer.CloudUser { + if self.id._internalGetInt64Value() == 777000 { + return true + } + } + return false + } + var isReplies: Bool { if self.namespace == Namespaces.Peer.CloudUser { if self.id._internalGetInt64Value() == 708513 || self.id._internalGetInt64Value() == 1271266957 { @@ -471,11 +483,26 @@ public extension PeerId { return false } + var isVerificationCodes: Bool { + if self.namespace == Namespaces.Peer.CloudUser { + if self.id._internalGetInt64Value() == 489000 { + return true + } + } + return false + } + + var isRepliesOrVerificationCodes: Bool { + return self.isReplies || self.isVerificationCodes + } + func isRepliesOrSavedMessages(accountPeerId: PeerId) -> Bool { if accountPeerId == self { return true } else if self.isReplies { return true + } else if self.isVerificationCodes { + return true } else { return false } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index d32cae1ad7d..ed541651403 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -286,7 +286,6 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) ), - linkHighlightColor: accentColor?.withAlphaComponent(0.3), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: accentColor?.withAlphaComponent(0.7), @@ -331,7 +330,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, - linkHighlightColor: day ? nil : accentColor?.withAlphaComponent(0.3), + linkHighlightColor: day ? nil : outgoingLinkTextColor?.withAlphaComponent(0.3), scamColor: outgoingScamColor, accentTextColor: outgoingAccentTextColor, accentControlColor: outgoingControlColor, @@ -1380,7 +1379,8 @@ public func defaultBuiltinWallpaper(data: BuiltinWallpaperData, colors: [UInt32] attributes: [ .ImageSize(size: PixelDimensions(width: 1440, height: 2960)), .FileName(fileName: "pattern.tgv") - ] + ], + alternativeRepresentations: [] ), settings: WallpaperSettings(colors: colors, intensity: intensity, rotation: rotation) )) diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index a5e38bb28f4..8af0bd3bc05 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -122,7 +122,7 @@ struct TelegramWallpaperStandardizedCodable: Codable { } if let slug = slug { - self.value = .file(TelegramWallpaper.File(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: !colors.isEmpty, isDark: false, slug: slug, file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: WallpaperDataResource(slug: slug), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, colors: colors.map { $0.argb }, intensity: intensity, rotation: rotation))) + self.value = .file(TelegramWallpaper.File(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: !colors.isEmpty, isDark: false, slug: slug, file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: WallpaperDataResource(slug: slug), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), settings: WallpaperSettings(blur: blur, motion: motion, colors: colors.map { $0.argb }, intensity: intensity, rotation: rotation))) } else if colors.count > 1 { self.value = .gradient(TelegramWallpaper.Gradient(id: nil, colors: colors.map { $0.argb }, settings: WallpaperSettings(blur: blur, motion: motion, rotation: rotation))) } else { diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index da4a919a0ee..5143dcad646 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -543,6 +543,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonIncomingProfileIconImage: UIImage public let chatBubbleActionButtonIncomingAddToChatIconImage: UIImage public let chatBubbleActionButtonIncomingWebAppIconImage: UIImage + public let chatBubbleActionButtonIncomingCopyIconImage: UIImage public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage @@ -553,6 +554,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonOutgoingProfileIconImage: UIImage public let chatBubbleActionButtonOutgoingAddToChatIconImage: UIImage public let chatBubbleActionButtonOutgoingWebAppIconImage: UIImage + public let chatBubbleActionButtonOutgoingCopyIconImage: UIImage public let chatEmptyItemLockIcon: UIImage public let emptyChatListCheckIcon: UIImage @@ -598,6 +600,8 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonIncomingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingAddToChatIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotAddToChat"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingWebAppIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotWebApp"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonIncomingCopyIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotCopy"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! @@ -607,6 +611,7 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonOutgoingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingAddToChatIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotAddToChat"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingWebAppIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotWebApp"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonOutgoingCopyIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotCopy"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 0f5c23f5bfe..07d33e5e237 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -65,9 +65,18 @@ private func renderIcon(name: String, scaleFactor: CGFloat = 1.0, backgroundColo public struct PresentationResourcesSettings { // MARK: Nicegram - public static let aiChatIcon = renderIcon(name: "ng.aichat.avatar") - public static let nicegramIcon = renderIcon(name: "logo-nicegram", backgroundColors: [.black], customSize: CGSize(width: 28, height: 28)) - public static let premiumIcon = nicegramIcon + public static var ngAiChatIcon: UIImage? { + UIImage(bundleImageName: "ng-settings/ai-chatbot") + } + public static var ngPremiumIcon: UIImage? { + UIImage(bundleImageName: "ng-settings/premium") + } + public static var ngSettingsIcon: UIImage? { + UIImage(bundleImageName: "ng-settings/settings") + } + public static var ngWalletIcon: UIImage? { + UIImage(bundleImageName: "ng-settings/wallet") + } // public static let editProfile = renderIcon(name: "Settings/Menu/EditProfile") @@ -118,6 +127,24 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) + public static let ton = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) + context.addPath(path.cgPath) + context.clip() + + context.setFillColor(UIColor(rgb: 0x32ade6).cgColor) + context.fill(bounds) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) + } + + drawBorder(context: context, rect: bounds) + }) + public static let stars = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index a548f9aac18..e289c562b6e 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -330,7 +330,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil return .file(performer) } } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if file.isAnimated { result = .animation } else { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 6e8bc0a8989..1ca5659fc2b 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -34,8 +34,8 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, E return result } -public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool) -> (text: String, spoilerRanges: [NSRange], customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)])? { - if let attributedString = universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList, forForumOverview: forForumOverview) { +public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> (text: String, spoilerRanges: [NSRange], customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)])? { + if let attributedString = universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList, forForumOverview: forForumOverview, forAdditionalServiceMessage: forAdditionalServiceMessage) { var ranges: [NSRange] = [] var customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)] = [] attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in @@ -79,7 +79,7 @@ private func peerDisplayTitles(_ peers: [Peer], strings: PresentationStrings, na } } -public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool) -> NSAttributedString? { +public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? { var attributedString: NSAttributedString? let primaryTextColor: UIColor @@ -235,7 +235,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { type = .round } else { @@ -735,7 +735,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } case let .webViewData(text): attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor) - case let .giftPremium(currency, amount, _, _, _): + case let .giftPremium(currency, amount, _, _, _, _, _): let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) @@ -952,8 +952,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } - case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _): - if boostPeerId == nil, let currency, let amount { + case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, text, entities): + if !forAdditionalServiceMessage, let text { + let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) + attributedString = mutableAttributedString + } else if boostPeerId == nil, let currency, let amount { let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) @@ -1051,6 +1054,26 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) + case let .starGift(gift, _, text, entities, _, _, _): + if !forAdditionalServiceMessage, let text { + let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) + attributedString = mutableAttributedString + } else { + let starsPrice = strings.Notification_StarsGift_Stars(Int32(gift.price)) + var authorName = compactAuthorName + var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] + if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { + authorName = strings.Notification_StarsGift_UnknownUser + peerIds = [] + } + if message.author?.id == accountPeerId { + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c3374307841..f5e49e1fd65 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -10,7 +10,7 @@ public func formatTonAddress(_ address: String) -> String { return address } -public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, dateTimeFormat: PresentationDateTimeFormat) -> String { +public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double = 1.0, dateTimeFormat: PresentationDateTimeFormat) -> String { let decimalSeparator = dateTimeFormat.decimalSeparator let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) var formattedValue = String(format: "%0.2f", normalizedValue * rate) @@ -27,15 +27,15 @@ public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, return "$\(formattedValue)" } -public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String { +public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = "\(abs(value))" while balanceText.count < 10 { balanceText.insert("0", at: balanceText.startIndex) } - balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) + balanceText.insert(contentsOf: dateTimeFormat.decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) while true { if balanceText.hasSuffix("0") { - if balanceText.hasSuffix("\(decimalSeparator)0") { + if balanceText.hasSuffix("\(dateTimeFormat.decimalSeparator)0") { balanceText.removeLast() balanceText.removeLast() break @@ -46,16 +46,31 @@ public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPl break } } + + if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { + balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))]) + + let integerPartString = balanceText[.. Void let dismiss: () -> Void init( context: AccountContext, + mode: AdsInfoScreen.Mode, openPremium: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode self.openPremium = openPremium self.dismiss = dismiss } @@ -175,7 +186,7 @@ private final class ScrollContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Respect_Title, titleColor: textColor, - text: strings.AdsInfo_Respect_Text, + text: component.mode == .bot ? strings.AdsInfo_Bot_Respect_Text : strings.AdsInfo_Respect_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Ads/Privacy", @@ -187,9 +198,9 @@ private final class ScrollContent: CombinedComponent { AnyComponentWithIdentity( id: "split", component: AnyComponent(ParagraphComponent( - title: strings.AdsInfo_Split_Title, + title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, titleColor: textColor, - text: strings.AdsInfo_Split_Text, + text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Ads/Split", @@ -203,7 +214,7 @@ private final class ScrollContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Ads_Title, titleColor: textColor, - text: strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string, + text: component.mode == .bot ? strings.AdsInfo_Bot_Ads_Text : strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/BoostPerk/NoAds", @@ -242,7 +253,7 @@ private final class ScrollContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - var infoString = strings.AdsInfo_Launch_Text + var infoString = component.mode == .bot ? strings.AdsInfo_Bot_Launch_Text : strings.AdsInfo_Launch_Text if let spaceRegex { let nsRange = NSRange(infoString.startIndex..., in: infoString) let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange) @@ -328,27 +339,48 @@ private final class ScrollContent: CombinedComponent { private final class ContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment + class ExternalState { + var contentHeight: CGFloat = 0.0 + } + let context: AccountContext + let mode: AdsInfoScreen.Mode + let externalState: ExternalState let openPremium: () -> Void + let openContextMenu: () -> Void + let dismiss: () -> Void init( context: AccountContext, - openPremium: @escaping () -> Void + mode: AdsInfoScreen.Mode, + externalState: ExternalState, + openPremium: @escaping () -> Void, + openContextMenu: @escaping () -> Void, + dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode + self.externalState = externalState self.openPremium = openPremium + self.openContextMenu = openContextMenu + self.dismiss = dismiss } static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } final class State: ComponentState { var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? + + var cachedMoreImage: (UIImage, PresentationTheme)? } func makeState() -> State { @@ -358,18 +390,16 @@ private final class ContainerComponent: CombinedComponent { static var body: Body { let background = Child(Rectangle.self) let scroll = Child(ScrollComponent.self) - let bottomPanel = Child(BlurredBackgroundComponent.self) - let bottomSeparator = Child(Rectangle.self) - let actionButton = Child(SolidRoundedButtonComponent.self) let scrollExternalState = ScrollComponent.ExternalState() + let moreButton = Child(Button.self) + return { context in let environment = context.environment[EnvironmentType.self] - let theme = environment.theme - let strings = environment.strings let state = context.state - let controller = environment.controller + let openContextMenu = context.component.openContextMenu + let dismiss = context.component.dismiss let background = background.update( component: Rectangle(color: environment.theme.list.plainBackgroundColor), @@ -385,9 +415,10 @@ private final class ContainerComponent: CombinedComponent { component: ScrollComponent( content: AnyComponent(ScrollContent( context: context.component.context, + mode: context.component.mode, openPremium: context.component.openPremium, dismiss: { - controller()?.dismiss() + dismiss() } )), externalState: scrollExternalState, @@ -406,129 +437,40 @@ private final class ContainerComponent: CombinedComponent { availableSize: context.availableSize, transition: context.transition ) + context.component.externalState.contentHeight = scrollExternalState.contentHeight context.add(scroll .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let bottomPanelAlpha: CGFloat - if scrollExternalState.contentHeight > context.availableSize.height { - if let bottomContentOffset = state.bottomContentOffset { - bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + if case .bot = context.component.mode { + let moreImage: UIImage + if let (image, theme) = state.cachedMoreImage, theme === environment.theme { + moreImage = image } else { - bottomPanelAlpha = 1.0 + moreImage = generateMoreButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + state.cachedMoreImage = (moreImage, environment.theme) } - } else { - bottomPanelAlpha = 0.0 + let moreButton = moreButton.update( + component: Button( + content: AnyComponent(Image(image: moreImage)), + action: { + openContextMenu() + } + ).tagged(moreTag), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(moreButton + .position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 13.0 + moreButton.size.height / 2.0)) + ) } - let bottomPanel = bottomPanel.update( - component: BlurredBackgroundComponent( - color: theme.rootController.tabBar.backgroundColor - ), - availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight), - transition: context.transition - ) - let bottomSeparator = bottomSeparator.update( - component: Rectangle( - color: theme.rootController.tabBar.separatorColor - ), - availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), - transition: context.transition - ) - - context.add(bottomPanel - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) - .opacity(bottomPanelAlpha) - ) - context.add(bottomSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) - .opacity(bottomPanelAlpha) - ) - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let actionButton = actionButton.update( - component: SolidRoundedButtonComponent( - title: strings.AdsInfo_Understood, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: theme.list.itemCheckColors.fillColor, - backgroundColors: [], - foregroundColor: theme.list.itemCheckColors.foregroundColor - ), - font: .bold, - fontSize: 17.0, - height: buttonHeight, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - action: { - controller()?.dismiss() - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - context.add(actionButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0)) - ) - return context.availableSize } } } -public final class AdsInfoScreen: ViewControllerComponentContainer { - private let context: AccountContext - - public init( - context: AccountContext, - forceDark: Bool = false - ) { - self.context = context - - var openPremiumImpl: (() -> Void)? - super.init( - context: context, - component: ContainerComponent( - context: context, - openPremium: { - openPremiumImpl?() - } - ), - navigationBarAppearance: .none, - statusBarStyle: .ignore, - theme: forceDark ? .dark : .default - ) - - self.navigationPresentation = .modal - - openPremiumImpl = { [weak self] in - guard let self else { - return - } - - let navigationController = self.navigationController - self.dismiss() - - Queue.mainQueue().after(0.3) { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) - navigationController?.pushViewController(controller, animated: true) - } - } - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - private final class ParagraphComponent: CombinedComponent { let title: String let titleColor: UIColor @@ -673,3 +615,939 @@ private final class ParagraphComponent: CombinedComponent { } } } + + +public class AdsInfoScreen: ViewController { + public enum Mode: Equatable { + case channel + case bot + } + + final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: AdsInfoScreen? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + + let contentView: ComponentHostView + let footerContainerView: UIView + let footerView: ComponentHostView + + private var containerExternalState = ContainerComponent.ExternalState() + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?)? + + private let hapticFeedback = HapticFeedback() + + private var currentIsVisible: Bool = false + private var currentLayout: ContainerViewLayout? + + init(context: AccountContext, controller: AdsInfoScreen) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + if controller.forceDark { + self.presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = self.presentationData.withUpdated(theme: self.presentationData.theme.withModalBlocksBackground()) + + self.controller = controller + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.contentView = ComponentHostView() + + self.footerContainerView = UIView() + self.footerView = ComponentHostView() + + super.init() + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.contentView) + + self.containerView.addSubview(self.footerContainerView) + self.footerContainerView.addSubview(self.footerView) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self.wrappedGestureRecognizerDelegate + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let layout = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + if let scrollView = otherGestureRecognizer.view as? UIScrollView { + if scrollView.contentSize.width > scrollView.contentSize.height { + return false + } + } + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + + func requestLayout(transition: ComponentTransition) { + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) + } + + private var dismissOffset: CGFloat? + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: ComponentTransition) { + guard !self.isDismissing else { + return + } + self.currentLayout = layout + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + let isLandscape = layout.orientation == .landscape + + var containerTopInset: CGFloat = 0.0 + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + containerTopInset = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + self.updated(transition: transition, forceUpdate: forceUpdate) + + let contentHeight = self.containerExternalState.contentHeight + if contentHeight > 0.0 && contentHeight < 400.0, let view = self.footerView.componentView as? FooterComponent.View { + view.backgroundView.alpha = 0.0 + view.separator.opacity = 0.0 + } + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else if let dismissOffset = self.dismissOffset, !dismissOffset.isZero { + topInset = edgeTopInset * dismissOffset + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let footerHeight = self.footerHeight + let convertedFooterFrame = self.view.convert(CGRect(origin: CGPoint(x: clipFrame.minX, y: clipFrame.maxY - footerHeight), size: CGSize(width: clipFrame.width, height: footerHeight)), to: self.containerView) + transition.setFrame(view: self.footerContainerView, frame: convertedFooterFrame) + } + + func updated(transition: ComponentTransition, forceUpdate: Bool = false) { + guard let controller = self.controller, let layout = self.currentLayout else { + return + } + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: layout.metrics.orientation, + isVisible: self.currentIsVisible, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + let contentSize = self.contentView.update( + transition: transition, + component: AnyComponent( + ContainerComponent( + context: controller.context, + mode: controller.mode, + externalState: self.containerExternalState, + openPremium: { [weak self] in + guard let self, let controller = self.controller else { + return + } + + let context = controller.context + let forceDark = controller.forceDark + let navigationController = controller.navigationController + controller.dismiss(animated: true) + + Queue.mainQueue().after(0.3) { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: forceDark, dismissed: nil) + navigationController?.pushViewController(controller, animated: true) + } + }, + openContextMenu: { [weak self] in + guard let self else { + return + } + self.infoPressed() + }, + dismiss: { [weak self] in + guard let self, let controller = self.controller else { + return + } + controller.dismiss(animated: true) + } + ) + ), + environment: { environment }, + forceUpdate: forceUpdate, + containerSize: self.containerView.bounds.size + ) + self.contentView.frame = CGRect(origin: .zero, size: contentSize) + + let footerHeight = self.footerHeight + let footerSize = self.footerView.update( + transition: .immediate, + component: AnyComponent( + FooterComponent( + context: controller.context, + theme: self.presentationData.theme, + title: self.presentationData.strings.AdsInfo_Understood, + action: { [weak self] in + guard let self else { + return + } + self.buttonPressed() + } + ) + ), + environment: {}, + containerSize: CGSize(width: self.containerView.bounds.width, height: footerHeight) + ) + self.footerView.frame = CGRect(origin: .zero, size: footerSize) + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var footerHeight: CGFloat { + guard let layout = self.currentLayout else { + return 58.0 + } + + var footerHeight: CGFloat = 8.0 + 50.0 + footerHeight += layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : 8.0 + return footerHeight + } + + private var defaultTopInset: CGFloat { + guard let layout = self.currentLayout else { + return 210.0 + } + if case .compact = layout.metrics.widthClass { + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : bottomPanelPadding + let panelHeight: CGFloat = bottomPanelPadding + 50.0 + bottomInset + 28.0 + + var defaultTopInset = layout.size.height - layout.size.width - 128.0 - panelHeight + + let containerTopInset = 10.0 + (layout.statusBarHeight ?? 0.0) + let contentHeight = self.containerExternalState.contentHeight + let footerHeight = self.footerHeight + if contentHeight > 0.0 { + let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0 + if delta > 0.0 { + defaultTopInset += delta + } + } + return defaultTopInset + } else { + return 210.0 + } + } + + private func findVerticalScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView, view.contentSize.height > view.contentSize.width { + return view + } + return findVerticalScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let layout = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollView = self.findVerticalScrollView(view: currentHitView) + if scrollView?.frame.height == self.frame.width { + scrollView = nil + } + if scrollView?.isDescendant(of: self.view) == false { + scrollView = nil + } + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView) + case .changed: + guard let (topInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + if scrollView == nil { + translation = max(0.0, translation) + } + + self.panGestureArguments = (topInset, translation, scrollView) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } else { + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func updateDismissOffset(_ offset: CGFloat) { + guard self.isExpanded, let layout = self.currentLayout else { + return + } + + self.dismissOffset = offset + self.containerLayoutUpdated(layout: layout, transition: .immediate) + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.dismissOffset = nil + self.isExpanded = isExpanded + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } + + func displayUndo(_ content: UndoOverlayContent) { + guard let controller = self.controller else { + return + } + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + } + + func infoPressed() { + guard let referenceView = self.contentView.findTaggedView(tag: moreTag), let controller = self.controller, let message = controller.message, let adAttribute = message.adAttribute else { + return + } + + let context = controller.context + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + + var actions: [ContextMenuItem] = [] + if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + var subItems: [ContextMenuItem] = [] + + subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, iconPosition: .left, action: { c, _ in + c?.popItems() + }))) + + subItems.append(.separator) + + if let sponsorInfo = adAttribute.sponsorInfo { + subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return nil + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = sponsorInfo + + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + }))) + } + if let additionalInfo = adAttribute.additionalInfo { + subItems.append(.action(ContextMenuActionItem(text: additionalInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return nil + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = additionalInfo + + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + }))) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) + }))) + } + + let removeAd = self.controller?.removeAd + if adAttribute.canReport { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.default) + + guard let navigationController = self?.controller?.navigationController as? NavigationController else { + return + } + + self?.controller?.dismiss(animated: true) + + let _ = (context.engine.messages.reportAdMessage(peerId: message.id.peerId, opaqueId: adAttribute.opaqueId, option: nil) + |> deliverOnMainQueue).start(next: { [weak navigationController] result in + if case let .options(title, options) = result { + Queue.mainQueue().after(0.2) { + navigationController?.pushViewController( + AdsReportScreen( + context: context, + peerId: message.id.peerId, + opaqueId: adAttribute.opaqueId, + title: title, + options: options, + completed: { + removeAd?(adAttribute.opaqueId) + } + ) + ) + } + } + }) + }))) + + actions.append(.separator) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd?(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + }))) + } else { + if !actions.isEmpty { + actions.append(.separator) + } + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd?(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + func presentNoAdsDemo() { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + let context = controller.context + var replaceImpl: ((ViewController) -> Void)? + let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak demoController] c in + demoController?.replace(with: c) + } + controller.dismiss(animated: true) + Queue.mainQueue().after(0.4) { + navigationController.pushViewController(demoController) + } + } + + func buttonPressed() { + self.controller?.dismiss(animated: true) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let mode: Mode + private let message: Message? + private let forceDark: Bool + + private var currentLayout: ContainerViewLayout? + + public var removeAd: (Data) -> Void = { _ in } + + public init( + context: AccountContext, + mode: Mode, + message: Message? = nil, + forceDark: Bool = false + ) { + self.context = context + self.mode = mode + self.message = message + self.forceDark = forceDark + + super.init(navigationBarPresentationData: nil) + + self.navigationPresentation = .flatModal + self.statusBar.statusBarStyle = .Ignore + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self) + self.displayNodeDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + self.node.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } +} + +private final class FooterComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let title: String + let action: () -> Void + + init(context: AccountContext, theme: PresentationTheme, title: String, action: @escaping () -> Void) { + self.context = context + self.theme = theme + self.title = title + self.action = action + } + + static func ==(lhs: FooterComponent, rhs: FooterComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + let backgroundView: BlurredBackgroundView + let separator = SimpleLayer() + + private let button = ComponentView() + + private var component: FooterComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil) + + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separator) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: FooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let bounds = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.updateColor(color: component.theme.rootController.tabBar.backgroundColor, transition: transition.containedViewLayoutTransition) + self.backgroundView.update(size: bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundView, frame: bounds) + + self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor + transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent( + SolidRoundedButtonComponent( + title: component.title, + theme: SolidRoundedButtonComponent.Theme(theme: component.theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + animationName: nil, + iconPosition: .left, + action: { + component.action() + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: availableSize.height) + ) + + if let view = self.button.view { + if view.superview == nil { + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: buttonSize) + view.frame = buttonFrame + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateMoreButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(foregroundColor.cgColor) + + let circleSize = CGSize(width: 4.0, height: 4.0) + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0), y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) - circleSize.width - 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) + circleSize.width + 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + }) +} + +private final class AdsInfoContextReferenceContentSource: ContextReferenceContentSource { + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets + + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + self.controller = controller + self.sourceView = sourceView + self.insets = insets + self.contentInsets = contentInsets + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) + } +} diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 86ee3fe125d..34da6c35b24 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -228,7 +228,8 @@ private final class SheetPageContent: CombinedComponent { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.ReportAd_Help_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } )), - items: items + items: items, + isModal: true ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), @@ -396,6 +397,7 @@ private final class SheetContent: CombinedComponent { let navigation = navigation.update( component: NavigationStackComponent( items: items, + clipContent: false, requestPop: { [weak state] in state?.pushedOptions.removeLast() update(.spring(duration: 0.45)) diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD index 553ce77a7db..3a3d52d8870 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index bb26607690c..b3ead20bb77 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData public final class AnimatedTextComponent: Component { public struct Item: Equatable { @@ -24,15 +25,18 @@ public final class AnimatedTextComponent: Component { public let font: UIFont public let color: UIColor public let items: [Item] + public let noDelay: Bool public init( font: UIFont, color: UIColor, - items: [Item] + items: [Item], + noDelay: Bool = false ) { self.font = font self.color = color self.items = items + self.noDelay = noDelay } public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { @@ -45,6 +49,9 @@ public final class AnimatedTextComponent: Component { if lhs.items != rhs.items { return false } + if lhs.noDelay != rhs.noDelay { + return false + } return true } @@ -157,10 +164,12 @@ public final class AnimatedTextComponent: Component { if animateIn, !transition.animation.isImmediate { var delayWidth: Double = 0.0 - if let firstDelayWidth { - delayWidth = size.width - firstDelayWidth - } else { - firstDelayWidth = size.width + if !component.noDelay { + if let firstDelayWidth { + delayWidth = size.width - firstDelayWidth + } else { + firstDelayWidth = size.width + } } characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) @@ -220,3 +229,33 @@ public final class AnimatedTextComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public extension AnimatedTextComponent { + static func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { + var textItems: [AnimatedTextComponent.Item] = [] + + var previousIndex = 0 + let nsString = string.string as NSString + for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if let value = mapping[range.index] { + let isUnbreakable: Bool + switch value { + case .text: + isUnbreakable = true + case .number: + isUnbreakable = false + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) + } + previousIndex = range.range.upperBound + } + if nsString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) + } + + return textItems + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 29fcb45f514..7a8fb12ea52 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -17,9 +17,9 @@ private func alignUp(size: Int, align: Int) -> Int { private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } diff --git a/submodules/TelegramUI/Components/BadgeComponent/BUILD b/submodules/TelegramUI/Components/BadgeComponent/BUILD new file mode 100644 index 00000000000..023edf4bb5f --- /dev/null +++ b/submodules/TelegramUI/Components/BadgeComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BadgeComponent", + module_name = "BadgeComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift b/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift new file mode 100644 index 00000000000..be4b3b3042f --- /dev/null +++ b/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift @@ -0,0 +1,184 @@ +import Foundation +import UIKit +import Display +import RasterizedCompositionComponent +import ComponentFlow + +public final class BadgeComponent: Component { + public let text: String + public let font: UIFont + public let cornerRadius: CGFloat + public let insets: UIEdgeInsets + public let outerInsets: UIEdgeInsets + + public init( + text: String, + font: UIFont, + cornerRadius: CGFloat, + insets: UIEdgeInsets, + outerInsets: UIEdgeInsets + ) { + self.text = text + self.font = font + self.cornerRadius = cornerRadius + self.insets = insets + self.outerInsets = outerInsets + } + + public static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.font != rhs.font { + return false + } + if lhs.cornerRadius != rhs.cornerRadius { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.outerInsets != rhs.outerInsets { + return false + } + return true + } + + private struct TextLayout { + var size: CGSize + var opticalBounds: CGRect + + init(size: CGSize, opticalBounds: CGRect) { + self.size = size + self.opticalBounds = opticalBounds + } + } + + public final class View: UIView { + override public static var layerClass: AnyClass { + return RasterizedCompositionLayer.self + } + + private let contentsClippingLayer: RasterizedCompositionLayer + private let backgroundInsetLayer: RasterizedCompositionImageLayer + private let backgroundLayer: RasterizedCompositionImageLayer + private let textContentsLayer: RasterizedCompositionImageLayer + + private var textLayout: TextLayout? + + private var component: BadgeComponent? + + override public init(frame: CGRect) { + self.contentsClippingLayer = RasterizedCompositionLayer() + self.backgroundInsetLayer = RasterizedCompositionImageLayer() + self.backgroundLayer = RasterizedCompositionImageLayer() + + self.textContentsLayer = RasterizedCompositionImageLayer() + self.textContentsLayer.anchorPoint = CGPoint() + + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundInsetLayer) + self.layer.addSublayer(self.backgroundLayer) + self.layer.addSublayer(self.contentsClippingLayer) + self.contentsClippingLayer.addSublayer(self.textContentsLayer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + + if component.text != previousComponent?.text || component.font != previousComponent?.font { + let attributedText = NSAttributedString(string: component.text, attributes: [ + NSAttributedString.Key.font: component.font, + NSAttributedString.Key.foregroundColor: UIColor.black + ]) + + var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil) + boundingRect.size.width = ceil(boundingRect.size.width) + boundingRect.size.height = ceil(boundingRect.size.height) + + if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) { + context.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + attributedText.draw(at: CGPoint()) + } + var minFilledLineY = Int(context.scaledSize.height) - 1 + var maxFilledLineY = 0 + var minFilledLineX = Int(context.scaledSize.width) - 1 + var maxFilledLineX = 0 + for y in 0 ..< Int(context.scaledSize.height) { + let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + for x in 0 ..< Int(context.scaledSize.width) { + let pixelPtr = linePtr.advanced(by: x) + if pixelPtr.pointee != 0 { + minFilledLineY = min(y, minFilledLineY) + maxFilledLineY = max(y, maxFilledLineY) + minFilledLineX = min(x, minFilledLineX) + maxFilledLineX = max(x, maxFilledLineX) + } + } + } + + var opticalBounds = CGRect() + if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY { + opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale + opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale + opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale + opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale + } + + self.textContentsLayer.image = context.generateImage() + self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds) + } else { + self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size)) + } + } + + if component.cornerRadius != previousComponent?.cornerRadius { + self.backgroundLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .white) + + self.backgroundInsetLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .black) + } + + let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0) + + let size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom) + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) + transition.setFrame(layer: self.contentsClippingLayer, frame: backgroundFrame) + + let outerInsetsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX - component.outerInsets.left, y: backgroundFrame.minY - component.outerInsets.top), size: CGSize(width: backgroundFrame.width + component.outerInsets.left + component.outerInsets.right, height: backgroundFrame.height + component.outerInsets.top + component.outerInsets.bottom)) + transition.setFrame(layer: self.backgroundInsetLayer, frame: outerInsetsFrame) + + var textFrame = CGRect(origin: CGPoint(x: component.insets.left, y: component.insets.top), size: textSize) + if let textLayout = self.textLayout { + textFrame.origin.x = -textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5) + textFrame.origin.y = -textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5) + } + + transition.setPosition(layer: self.textContentsLayer, position: textFrame.origin) + self.textContentsLayer.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index f3c401fcbb4..8f4595c8b92 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -80,7 +80,7 @@ public final class ButtonBadgeComponent: Component { if contentView.superview == nil { self.addSubview(contentView) } - transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) } if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height { @@ -264,7 +264,7 @@ public final class ButtonTextContentComponent: Component { size.height = max(size.height, badgeSize.height) } - let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - measurementSize.width) * 0.5), y: floor((size.height - measurementSize.height) * 0.5)), size: measurementSize) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize) if let contentView = self.content.view { if contentView.superview == nil { @@ -274,7 +274,7 @@ public final class ButtonTextContentComponent: Component { } if let badgeSize, let badge = self.badge { - let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) + let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) if let badgeView = badge.view { var animateIn = false @@ -490,7 +490,7 @@ public final class ButtonComponent: Component { contentView.isUserInteractionEnabled = false self.addSubview(contentView) } - let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: floor((availableSize.height - contentSize.height) * 0.5)), size: contentSize) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize) contentTransition.setFrame(view: contentView, frame: contentFrame) contentTransition.setAlpha(view: contentView, alpha: contentAlpha) @@ -528,7 +528,7 @@ public final class ButtonComponent: Component { } let indicatorSize = CGSize(width: 22.0, height: 22.0) transition.setAlpha(view: activityIndicator.view, alpha: 1.0) - activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - indicatorSize.width) / 2.0), y: floor((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) } else { if let activityIndicator = self.activityIndicator { self.activityIndicator = nil diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index 2e98539da1a..d98cb1740a9 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +NGDEPS = [ + "//Nicegram/NGResources:NGResources" +] + load( "@build_bazel_rules_apple//apple:resources.bzl", "apple_resource_bundle", @@ -69,6 +73,8 @@ swift_library( "//submodules/AppBundle", "//submodules/UIKitRuntimeUtils", "//submodules/TelegramPresentationData", + "//Nicegram/NGCallRecorder:NGCallRecorder", + "//Nicegram/NGData:NGData" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 4c731b68f83..16f79d5566e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -14,6 +14,9 @@ final class ButtonGroupView: OverlayMaskContainerView { case video case microphone case end +// MARK: Nicegram NCG-5828 call recording + case record +// } case speaker(audioOutput: PrivateCallScreen.State.AudioOutput) @@ -21,6 +24,7 @@ final class ButtonGroupView: OverlayMaskContainerView { case video(isActive: Bool) case microphone(isMuted: Bool) case end + case record(isRecord: Bool) var key: Key { switch self { @@ -34,6 +38,10 @@ final class ButtonGroupView: OverlayMaskContainerView { return .microphone case .end: return .end +// MARK: Nicegram NCG-5828 call recording + case .record: + return .record +// } } } @@ -90,8 +98,9 @@ final class ButtonGroupView: OverlayMaskContainerView { self.buttons = buttons let buttonSize: CGFloat = 56.0 - let buttonSpacing: CGFloat = 36.0 - +// MARK: Nicegram NCG-5828 call recording + let buttonSpacing: CGFloat = buttons.count > 4 ? 17.0 : 36.0 +// let buttonNoticeSpacing: CGFloat = 16.0 let controlsHiddenNoticeSpacing: CGFloat = 0.0 var nextNoticeY: CGFloat @@ -264,6 +273,12 @@ final class ButtonGroupView: OverlayMaskContainerView { image = UIImage(bundleImageName: "Call/End") isActive = false isDestructive = true +// MARK: Nicegram NCG-5828 call recording + case let .record(isRecord): + title = "record" + image = isRecord ? UIImage(bundleImageName: "RecordStop") : UIImage(bundleImageName: "RecordStart") + isActive = isRecord +// } var buttonTransition = transition diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift index 1fcba0be180..da2744a7c3b 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift @@ -130,7 +130,7 @@ public final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { let phase = self.phase let blobs = self.blobs - context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0)), edgeInset: 4), state: RenderState.self, layer: self, commands: { encoder, placement in + context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0)), edgeInset: 2), state: RenderState.self, layer: self, commands: { encoder, placement in let rect = placement.effectiveRect for i in 0 ..< blobs.count { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 28fcc277ad3..684d00fcf60 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -8,6 +8,10 @@ import ComponentFlow import SwiftSignalKit import UIKitRuntimeUtils import TelegramPresentationData +// MARK: Nicegram NCG-5828 call recording +import NGData +import NGCallRecorder +// public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate { public struct State: Equatable { @@ -80,7 +84,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool public var isEnergySavingEnabled: Bool - +// MARK: Nicegram NCG-5828 call recording + public var isCallRecord: Bool +// + // MARK: Nicegram NCG-5828 call recording, isCallRecord public init( strings: PresentationStrings, lifecycleState: LifecycleState, @@ -93,7 +100,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool, - isEnergySavingEnabled: Bool + isEnergySavingEnabled: Bool, + isCallRecord: Bool ) { self.strings = strings self.lifecycleState = lifecycleState @@ -107,6 +115,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow self.isEnergySavingEnabled = isEnergySavingEnabled + self.isCallRecord = isCallRecord } public static func ==(lhs: State, rhs: State) -> Bool { @@ -146,6 +155,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if lhs.isEnergySavingEnabled != rhs.isEnergySavingEnabled { return false } +// MARK: Nicegram NCG-5828 call recording + if lhs.isCallRecord != rhs.isCallRecord { + return false + } +// return true } } @@ -324,19 +338,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.closeAction?() } - if #available(iOS 16.0, *) { - let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() - pipVideoCallViewController.view.addSubview(self.pipView) - self.pipView.frame = pipVideoCallViewController.view.bounds - self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.pipView.translatesAutoresizingMaskIntoConstraints = true - self.pipVideoCallViewController = pipVideoCallViewController + if !"".isEmpty { + if #available(iOS 16.0, *) { + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.view.addSubview(self.pipView) + self.pipView.frame = pipVideoCallViewController.view.bounds + self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.pipView.translatesAutoresizingMaskIntoConstraints = true + self.pipVideoCallViewController = pipVideoCallViewController + } } if let blurFilter = makeBlurFilter() { blurFilter.setValue(10.0 as NSNumber, forKey: "inputRadius") self.overlayContentsView.layer.filters = [blurFilter] } +// MARK: Nicegram NCG-5828 call recording + self.buttonGroupView.addSubview(self.recordTimerView) +// } public required init?(coder: NSCoder) { @@ -733,6 +752,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu guard let self else { return } +// MARK: Nicegram NCG-5828 call recording + self.stopRecordTimer() +// self.endCallAction?() }) ] @@ -751,7 +773,34 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.speakerAction?() }), at: 0) } - +// MARK: Nicegram NCG-5828 call recording + if case .active = params.state.lifecycleState, + isPremium() { + if NGSettings.recordAllCalls && + !params.state.isCallRecord && + isRecordAllCallsFirstStart { + isRecordAllCallsFirstStart = false + startRecordTimer() + recordAction?() + } + buttons.insert( + ButtonGroupView.Button( + content: .record(isRecord: params.state.isCallRecord), + isEnabled: !isTerminated, + action: { [weak self] in + guard let self else { + return + } + if !params.state.isCallRecord { + self.startRecordTimer() + } + self.recordAction?() + } + ), + at: 2 + ) + } +// var notices: [ButtonGroupView.Notice] = [] if !isTerminated { if params.state.isLocalAudioMuted { @@ -1375,7 +1424,93 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu }) } } +// MARK: Nicegram NCG-5828 call recording + updateRecordTimerView(with: transition, currentAreControlsHidden: currentAreControlsHidden) +// + } + +// MARK: Nicegram NCG-5828 call recording + public var recordAction: (() -> Void)? + + private let recordTimerView = RecordIndicatorView() + + private var recordTimer: SwiftSignalKit.Timer? + private var isRecordAllCallsFirstStart: Bool = true + + private func startRecordTimer() { + var duration: Int = 1 + + let recordTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + + let size = self.recordTimerView.update( + text: self.stringForDuration(duration), + transition: .immediate + ) + + self.recordTimerView.frame = .init( + origin: self.recordTimerView.frame.origin, + size: size + ) + duration += 1 + }, queue: .mainQueue()) + self.recordTimer = recordTimer + recordTimer.start() + recordTimerView.animateIn() + } + + public func stopRecordTimer() { + if let recordTimer { + recordTimer.invalidate() + self.recordTimer = nil + + recordTimerView.animateOut { [weak self] in + _ = self?.recordTimerView.update( + text: "0:00", + transition: .immediate + ) + } + } + } + + private func updateRecordTimerView( + with transition: ComponentTransition, + currentAreControlsHidden: Bool + ) { + let center = buttonGroupView.frame.center + var size = recordTimerView.frame.size + if size == .zero { + size = recordTimerView.update( + text: "0:00", + transition: .immediate + ) + } + + var recordTimerViewY = frame.height - (size.height + 30.0) + if currentAreControlsHidden { + recordTimerViewY = frame.height + (size.height + 30.0) + } + let recordTimerViewFrame = CGRect( + origin: .init( + x: center.x - size.width / 2, + y: recordTimerViewY + ), + size: recordTimerView.frame.size + ) + + transition.setFrame(view: recordTimerView, frame: recordTimerViewFrame) + } + + private func stringForDuration(_ duration: Int) -> String { + let minutes = duration / 60 + let seconds = duration % 60 + + + return String(format: "%d:%02d", minutes, seconds) } +// } final class SnowEffectView: UIView { diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD index 6720eda7209..71ae013f963 100644 --- a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD @@ -54,7 +54,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], data = [ ":VoiceChatActionButtonMetalSourcesBundle", diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index 8ee63a28acc..1769e8e2d38 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -151,7 +151,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { continuePlayingWithoutSoundOnLostAudioSession: false, storeAfterDownload: nil ) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.canAttachContent = true self.videoNode = videoNode @@ -511,7 +511,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -531,9 +531,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -591,8 +591,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -607,7 +607,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index 269f0a80d29..f58859eb8ef 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -438,6 +438,8 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { if let message = self.message { self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId, maxQuantity) } + case let .copyText(payload): + self.controllerInteraction.copyText(payload) } if dismissIfOnce { if let message = self.message { diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD index 70f4619d5bd..c30ddfd2694 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -1,9 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -NGDEPS = [ - "@swiftpkg_nicegram_assistant_ios//:FeatTasks", -] - swift_library( name = "ChatChannelSubscriberInputPanelNode", module_name = "ChatChannelSubscriberInputPanelNode", @@ -13,7 +9,7 @@ swift_library( copts = [ #"-warnings-as-errors", ], - deps = NGDEPS + [ + deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 115fd605314..dbc280b66ec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -1,6 +1,3 @@ -// MARK: Nicegram Tasks -import FeatTasks -// import Foundation import UIKit import AsyncDisplayKit @@ -251,18 +248,6 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { strongSelf.isJoining = false } } - - // MARK: Nicegram Tasks - if #available(iOS 15.0, *) { - Task { - let tryCompleteOngoingSubscribeTaskUseCase = TasksContainer.shared.tryCompleteOngoingSubscribeTaskUseCase() - - await tryCompleteOngoingSubscribeTaskUseCase( - channelId: peer.addressName ?? "" - ) - } - } - // }).startStrict(error: { [weak self] error in guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift index 67891754b99..5e55312bd93 100644 --- a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift @@ -172,7 +172,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = externalReference.content?.dimensions?.cgSize if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) imageResource = nil } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD index 1d884b45166..5558de6603f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +NGDEPS = [ + "@swiftpkg_nicegram_assistant_ios//:FeatAttentionEconomy", +] + swift_library( name = "ChatHistoryEntry", module_name = "ChatHistoryEntry", @@ -9,7 +13,7 @@ swift_library( copts = [ #"-warnings-as-errors", ], - deps = [ + deps = NGDEPS + [ "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 79be541f9a3..a61de31a34b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -1,3 +1,6 @@ +// MARK: Nicegram ATT +import FeatAttentionEconomy +// import Postbox import TelegramCore import TelegramPresentationData @@ -48,8 +51,30 @@ public enum ChatHistoryEntry: Identifiable, Comparable { case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData) case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData) case SearchEntry(PresentationTheme, PresentationStrings) + // MARK: Nicegram ATT + case NicegramAdEntry(String, AttAd, ChatPresentationData) + // - public var stableId: UInt64 { + // MARK: Nicegram ATT + public enum StableId: Hashable, Comparable { + case uint64(UInt64) + case nicegramAd(String) + + public var uint64Value: UInt64 { + switch self { + case let .uint64(uint64): uint64 + case .nicegramAd: 0 + } + } + + public static func <(lhs: StableId, rhs: StableId) -> Bool { + lhs.uint64Value < rhs.uint64Value + } + } + // + + // MARK: Nicegram ATT, changed UInt64 to StableId + public var stableId: StableId { switch self { case let .MessageEntry(message, _, _, _, _, attributes): let type: UInt64 @@ -61,17 +86,27 @@ public enum ChatHistoryEntry: Identifiable, Comparable { case .animatedEmoji: type = 4 } - return UInt64(message.stableId) | ((type << 40)) + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(message.stableId) | ((type << 40))) case let .MessageGroupEntry(groupInfo, _, _): - return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40)) + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))) case .UnreadEntry: - return UInt64(4) << 40 + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(4) << 40) case .ReplyCountEntry: - return UInt64(5) << 40 + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(5) << 40) case .ChatInfoEntry: - return UInt64(6) << 40 + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(6) << 40) case .SearchEntry: - return UInt64(7) << 40 + // MARK: Nicegram ATT, wrap in .uint64() + return .uint64(UInt64(7) << 40) + // MARK: Nicegram ATT + case let .NicegramAdEntry(id, _, _): + return .nicegramAd(id) + // } } @@ -89,6 +124,10 @@ public enum ChatHistoryEntry: Identifiable, Comparable { return MessageIndex.absoluteLowerBound() case .SearchEntry: return MessageIndex.absoluteLowerBound() + // MARK: Nicegram ATT + case .NicegramAdEntry: + return MessageIndex.absoluteLowerBound() + // } } @@ -106,6 +145,10 @@ public enum ChatHistoryEntry: Identifiable, Comparable { return MessageIndex.absoluteLowerBound() case .SearchEntry: return MessageIndex.absoluteLowerBound() + // MARK: Nicegram ATT + case .NicegramAdEntry: + return MessageIndex.absoluteLowerBound() + // } } @@ -284,6 +327,17 @@ public enum ChatHistoryEntry: Identifiable, Comparable { } else { return false } + // MARK: Nicegram ATT + case let .NicegramAdEntry(lhsId, lhsAd, lhsPresentationData): + if case let .NicegramAdEntry(rhsId, rhsAd, rhsPresentationData) = rhs, + lhsId == rhsId, + lhsAd == rhsAd, + lhsPresentationData === rhsPresentationData { + return true + } else { + return false + } + // } } diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 8edf85c6886..3a524f7488c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -648,7 +648,7 @@ public final class ChatInlineSearchResultsListComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index feaacffdb13..7b56c5b951f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -272,11 +272,11 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.mediaBackgroundNode.image = backgroundImage if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id { - let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index b52bbbe9782..9c71bdb71b2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -215,6 +215,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage case .openWebView: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage + case .copyText: + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingCopyIconImage : graphics.chatBubbleActionButtonOutgoingCopyIconImage default: iconImage = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 090041cb5ca..0b2300b7d21 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -343,15 +343,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if oldValue != self.visibility { - switch self.visibility { - case .none: - self.textNode.visibilityRect = nil - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - self.textNode.visibilityRect = subRect - } + self.updateVisibility() } } } @@ -594,6 +586,21 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + var effectiveVisibility = self.visibility + if !isPlaying { + effectiveVisibility = .none + } + + switch effectiveVisibility { + case .none: + self.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect + } + var canPlayEffects = isPlaying if !item.controllerInteraction.canReadHistory { canPlayEffects = false @@ -3063,7 +3070,7 @@ public struct AnimatedEmojiSoundsConfiguration { if let idString = dict["id"], let id = Int64(idString), let accessHashString = dict["access_hash"], let accessHash = Int64(accessHashString), let fileReference = Data(base64Encoded: fileReferenceString) { let resource = CloudDocumentMediaResource(datacenterId: 1, fileId: id, accessHash: accessHash, size: nil, fileReference: fileReference, fileName: nil) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: []) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: [], alternativeRepresentations: []) sounds[key] = file } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 6ef71e157fd..bcac2573376 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -221,6 +221,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } + let isAd = message.adAttribute != nil + var isReplyThread = false if case .replyThread = chatLocation { isReplyThread = true @@ -352,7 +354,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { contentFileValue = file } - if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file, isAd: isAd) { contentMediaAutomaticDownload = .full } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { contentMediaAutomaticDownload = .prefetch @@ -404,7 +406,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } else { let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue) + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue, isAd: isAd) let (_, initialImageWidth, refineLayout) = makeContentMedia( context, @@ -435,7 +437,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let contentFileContinueLayout: ChatMessageInteractiveFileNode.ContinueLayout? if let contentFileValue { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue) + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue, isAd: isAd) let (_, refineLayout) = makeContentFile(ChatMessageInteractiveFileNode.Arguments( context: context, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 5109dc625c7..a097d80a07c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -219,6 +219,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode { public var item: ChatMessageBubbleContentItem? public var updateIsTextSelectionActive: ((Bool) -> Void)? + public var requestInlineUpdate: (() -> Void)? open var disablesClipping: Bool { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 8be65d72247..1b62cf4a9d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -154,8 +154,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } + var messageMedia = message.media + if let updatingMedia = itemAttributes.updatingMedia, messageMedia.isEmpty, case let .update(media) = updatingMedia.media { + messageMedia.append(media.media) + } + var isFile = false - inner: for media in message.media { + inner: for media in messageMedia { if let media = media as? TelegramMediaPaidContent { var index = 0 for _ in media.extendedMedia { @@ -228,6 +233,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftStars = action.action { result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .starGift = action.action { + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + skipText = true } else if case .suggestedProfilePhoto = action.action { result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .setChatWallpaper = action.action { @@ -701,22 +709,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI override public var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { - for contentNode in self.contentNodes { - contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) - } - - if let threadInfoNode = self.threadInfoNode { - threadInfoNode.visibility = self.visibility != .none - } - - if let replyInfoNode = self.replyInfoNode { - replyInfoNode.visibility = self.visibility != .none - } - - if let unlockButtonNode = self.unlockButtonNode { - unlockButtonNode.visibility = self.visibility != .none - } - self.visibilityStatus = self.visibility != .none self.updateVisibility() @@ -739,6 +731,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private var forceStopAnimations: Bool = false + + typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: Bool) + private var currentInputParams: Params? + private var currentApplyParams: ListViewItemApply? + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -1319,24 +1317,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } recognizer.highlight = { [weak self] point in - if let strongSelf = self { - if strongSelf.selectionNode == nil { - if let replyInfoNode = strongSelf.replyInfoNode { - var translatedPoint: CGPoint? - let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) - if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) - } - replyInfoNode.updateTouchesAtPoint(translatedPoint) + if let strongSelf = self, strongSelf.selectionNode == nil { + if let replyInfoNode = strongSelf.replyInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) } - if let forwardInfoNode = strongSelf.forwardInfoNode { - var translatedPoint: CGPoint? - let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view) - if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view) - } - forwardInfoNode.updateTouchesAtPoint(translatedPoint) + replyInfoNode.updateTouchesAtPoint(translatedPoint) + } + if let forwardInfoNode = strongSelf.forwardInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view) } + forwardInfoNode.updateTouchesAtPoint(translatedPoint) } for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? @@ -1401,6 +1397,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private func internalUpdateLayout() { + if let inputParams = self.currentInputParams, let currentApplyParams = self.currentApplyParams { + let (_, applyLayout) = self.asyncLayout()(inputParams.item, inputParams.params, inputParams.mergedTop, inputParams.mergedBottom, inputParams.dateHeaderAtBottom) + applyLayout(.None, ListViewItemApply(isOnScreen: currentApplyParams.isOnScreen, timestamp: nil), false) + } + } + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { @@ -1460,7 +1463,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } private static func beginLayout( - selfReference: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool, + selfReference: Weak, + _ item: ChatMessageItem, + _ params: ListViewItemLayoutParams, + _ mergedTop: ChatMessageMerge, + _ mergedBottom: ChatMessageMerge, + _ dateHeaderAtBottom: Bool, currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), @@ -1585,11 +1593,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info { - if info.flags.contains(.messagesShouldHaveProfiles) { + if info.flags.contains(.messagesShouldHaveProfiles) && !item.presentationData.isPreview { var allowAuthor = incoming overrideEffectiveAuthor = true - if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview { + if let author = firstMessage.author, author is TelegramChannel, !incoming { allowAuthor = true ignoreNameHiding = true } @@ -1718,7 +1726,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let _ = sourceReference { needsShareButton = true } - } else if item.message.id.peerId.isReplies { + } else if item.message.id.peerId.isRepliesOrVerificationCodes { needsShareButton = false } else if incoming { if let _ = sourceReference { @@ -3187,6 +3195,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return (layout, { animation, applyInfo, synchronousLoads in return ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads, + inputParams: (item, params, mergedTop, mergedBottom, dateHeaderAtBottom), params: params, applyInfo: applyInfo, layout: layout, @@ -3246,6 +3255,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private static func applyLayout(selfReference: Weak, _ animation: ListViewItemUpdateAnimation, _ synchronousLoads: Bool, + inputParams: Params, params: ListViewItemLayoutParams, applyInfo: ListViewItemApply, layout: ListViewItemNodeLayout, @@ -3303,6 +3313,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + strongSelf.currentInputParams = inputParams + strongSelf.currentApplyParams = applyInfo + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { strongSelf.wasPending = true } @@ -4118,6 +4131,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in contextSourceNode?.updateDistractionFreeMode?(value) } + contentNode.requestInlineUpdate = { [weak strongSelf] in + guard let strongSelf else { + return + } + + strongSelf.internalUpdateLayout() + } contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview) } } @@ -5200,13 +5220,24 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.backgroundNode.frame.contains(location) { -// let message = item.message - if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { return .action(InternalBubbleTapAction.Action {}) } if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { - return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.content.firstMessage, selectAll: false, subFrame: self.backgroundNode.frame, disableDefaultPressAnimation: true)) + if self.selectionNode != nil, let attribute = item.message.attributes.first(where: { $0 is ReplyMessageAttribute }) as? ReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action({ [weak self] in + guard let self else { + return + } + var progress: Promise? + if let replyInfoNode = self.replyInfoNode { + progress = replyInfoNode.makeProgress() + } + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, progress: progress)) + }, contextMenuOnLongPress: true)) + } else { + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.content.firstMessage, selectAll: false, subFrame: self.backgroundNode.frame, disableDefaultPressAnimation: true)) + } } var tapMessage: Message? = item.content.firstMessage @@ -5285,12 +5316,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) - case let .hashtag(_, hashtag): + case let .hashtag(peerName, hashtag): + var fullHashtag = hashtag + if let peerName { + fullHashtag += "@\(peerName)" + } return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(hashtag, rects: rects) else { + guard let self, let contentNode = self.contextContentNodeForLink(fullHashtag, rects: rects) else { return } - item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + item.controllerInteraction.longTap(.hashtag(fullHashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .instantPage: break @@ -5648,10 +5683,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { switch item.content { - case let .message(message, _, _, _, _): + case let .message(message, _, _, _, _): item.controllerInteraction.toggleMessagesSelection([message.id], value) - case let .group(messages): - item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) + case let .group(messages): + item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) } } }) @@ -5901,7 +5936,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI @objc private func nameButtonPressed() { if let item = self.item, let peer = item.message.author { let messageReference = MessageReference(item.message) - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if peer.id.isVerificationCodes, let forwardAuthor = item.content.firstMessage.forwardInfo?.author { + if let channel = forwardAuthor as? TelegramChannel, case .broadcast = channel.info { + item.controllerInteraction.openPeer(EnginePeer(channel), .chat(textInputState: nil, subject: nil, peekData: nil), messageReference, .default) + } else { + item.controllerInteraction.openPeer(EnginePeer(forwardAuthor), .info(nil), messageReference, .default) + } + } else if let channel = peer as? TelegramChannel, case .broadcast = channel.info { item.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), messageReference, .default) } else { item.controllerInteraction.openPeer(EnginePeer(peer), .info(nil), messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: nil)) @@ -6302,14 +6343,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func getStatusNode() -> ASDisplayNode? { + if let statusNode = self.mosaicStatusNode { + return statusNode + } for contentNode in self.contentNodes { if let statusNode = contentNode.getStatusNode() { return statusNode } } - if let statusNode = self.mosaicStatusNode { - return statusNode - } return nil } @@ -6419,12 +6460,44 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return false } + override public func updateStickerSettings(forceStopAnimations: Bool) { + self.forceStopAnimations = forceStopAnimations + self.updateVisibility() + } + private func updateVisibility() { guard let item = self.item else { return } + let effectiveMediaVisibility = self.visibility + var isPlaying = true + if !item.controllerInteraction.canReadHistory { + isPlaying = false + } + + if self.forceStopAnimations { + isPlaying = false + } + + if !isPlaying { + self.removeEffectAnimations() + } + + var effectiveVisibility = self.visibility + if !isPlaying { + effectiveVisibility = .none + } + + for contentNode in self.contentNodes { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode { + contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + } else { + contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + } + } + if case let .visible(_, subRect) = self.visibility { if subRect.minY > 32.0 { isPlaying = false @@ -6432,12 +6505,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else { isPlaying = false } - if !item.controllerInteraction.canReadHistory { - isPlaying = false + + if let threadInfoNode = self.threadInfoNode { + threadInfoNode.visibility = effectiveVisibility != .none } - if !isPlaying { - self.removeEffectAnimations() + if let replyInfoNode = self.replyInfoNode { + replyInfoNode.visibility = effectiveVisibility != .none + } + + if let unlockButtonNode = self.unlockButtonNode { + unlockButtonNode.visibility = effectiveVisibility != .none } if isPlaying { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 7d076a23c51..f5510b86496 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -362,7 +362,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, - extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, + extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 251d8806121..a7518063228 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -70,7 +70,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess return strings.Message_RecommendedLabel } } - + var timestamp: Int32 if let scheduleTime = message.scheduleTime { timestamp = scheduleTime @@ -95,6 +95,10 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess dateText = " " } + if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute { + return "appx. \(dateText)" + } + if displayFullDate { let dayText: String diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/BUILD index fc309e1bda9..fc69651206f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Postbox", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index d52d60c7610..30bb04bf596 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -106,6 +106,9 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { selectedFile = telegramFile } } + if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile { + selectedFile = file + } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { @@ -135,7 +138,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) + let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile) let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( context: item.context, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD index ca9c72c12c5..aecc2427fdd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD @@ -30,6 +30,8 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 69497821c0e..b4eaa8b264b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -20,9 +20,11 @@ import ShimmerEffect import Markdown import ChatMessageBubbleContentNode import ChatMessageItemCommon +import TextNodeWithEntities +import InvisibleInkDustNode private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { - return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) } public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { @@ -31,23 +33,39 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let backgroundMaskNode: ASImageNode private var linkHighlightingNode: LinkHighlightingNode? + private let mediaBackgroundMaskNode: ASImageNode private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? - private let mediaBackgroundNode: NavigationBackgroundNode private let titleNode: TextNode - private let subtitleNode: TextNode + private let subtitleNode: TextNodeWithEntities + private var spoilerSubtitleNode: TextNodeWithEntities? + private let textClippingNode: ASDisplayNode + private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode + private let ribbonBackgroundNode: ASImageNode + private let ribbonTextNode: TextNode + private var shimmerEffectNode: ShimmerEffectForegroundNode? private let buttonNode: HighlightTrackingButtonNode private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode + private let moreTextNode: TextNode + + private var maskView: UIImageView? + private var maskOverlayView: UIView? + private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var absoluteRect: (CGRect, CGSize)? private var isPlaying: Bool = false + private var isExpanded: Bool = false + private var appliedIsExpanded: Bool = false + + private var isStarGift = false + private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { @@ -57,6 +75,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if wasVisible != isVisible { self.visibilityStatus = isVisible + + switch self.visibility { + case .none: + self.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.subtitleNode.visibilityRect = subRect + } } } } @@ -69,7 +97,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - private var animationDisposable: Disposable? + private var fetchDisposable: Disposable? private var setupTimestamp: Double? required public init() { @@ -79,17 +107,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.backgroundMaskNode = ASImageNode() - self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) - self.mediaBackgroundNode.clipsToBounds = true - self.mediaBackgroundNode.cornerRadius = 24.0 + self.mediaBackgroundMaskNode = ASImageNode() self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false - self.subtitleNode = TextNode() - self.subtitleNode.isUserInteractionEnabled = false - self.subtitleNode.displaysAsynchronously = false + self.subtitleNode = TextNodeWithEntities() + self.subtitleNode.textNode.isUserInteractionEnabled = false + self.subtitleNode.textNode.displaysAsynchronously = false + + self.textClippingNode = ASDisplayNode() + self.textClippingNode.clipsToBounds = true self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true @@ -107,31 +136,43 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.displaysAsynchronously = false + self.ribbonBackgroundNode = ASImageNode() + self.ribbonBackgroundNode.displaysAsynchronously = false + + self.ribbonTextNode = TextNode() + self.ribbonTextNode.isUserInteractionEnabled = false + self.ribbonTextNode.displaysAsynchronously = false + + self.moreTextNode = TextNode() + self.moreTextNode.isUserInteractionEnabled = false + self.moreTextNode.displaysAsynchronously = false + super.init() self.addSubnode(self.labelNode) - self.addSubnode(self.mediaBackgroundNode) self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode) + self.addSubnode(self.textClippingNode) + self.textClippingNode.addSubnode(self.subtitleNode.textNode) + self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) + self.addSubnode(self.moreTextNode) self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) - self.addSubnode(self.buttonTitleNode) + self.buttonNode.addSubnode(self.buttonTitleNode) + + self.addSubnode(self.ribbonBackgroundNode) + self.addSubnode(self.ribbonTextNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.alpha = 0.4 - strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonTitleNode.alpha = 0.4 } else { strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.buttonTitleNode.alpha = 1.0 - strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } @@ -144,10 +185,23 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } deinit { - self.animationDisposable?.dispose() + self.fetchDisposable?.dispose() self.currentProgressDisposable?.dispose() } + override public func didLoad() { + super.didLoad() + + self.maskView = UIImageView() + + let maskOverlayView = UIView() + maskOverlayView.alpha = 0.0 + maskOverlayView.backgroundColor = .white + self.maskOverlayView = maskOverlayView + + self.maskView?.addSubview(maskOverlayView) + } + @objc private func buttonPressed() { guard let item = self.item else { return @@ -155,6 +209,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress())) } + private func expandPressed() { + self.isExpanded = !self.isExpanded + guard let item = self.item else{ + return + } + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } + private func makeProgress() -> Promise { let progress = Promise() self.currentProgressDisposable?.dispose() @@ -224,11 +286,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) + let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) - + let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) + let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode) + let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage + let currentIsExpanded = self.isExpanded + return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -243,17 +311,31 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var months: Int32 = 3 var animationName: String = "" + var animationFile: TelegramMediaFile? var title = item.presentationData.strings.Notification_PremiumGift_Title var text = "" + var entities: [MessageTextEntity] = [] var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View + var ribbonTitle = "" var hasServiceMessage = true var textSpacing: CGFloat = 0.0 + var isStarGift = false for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .giftPremium(_, _, monthsValue, _, _): + case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities): months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } case let .giftStars(_, _, count, _, _, _): if count <= 1000 { months = 3 @@ -282,10 +364,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } title = item.presentationData.strings.Notification_StarsGiveaway_Title text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): + case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities): if channelId == nil { months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } if item.message.author?.id != item.context.account.peerId { buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift } @@ -314,6 +406,49 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): + isStarGift = true + let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + title = item.presentationData.strings.Notification_StarGift_Title(authorName).string + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + if incoming { + if converted { + text = item.presentationData.strings.Notification_StarGift_Subtitle_Converted(item.presentationData.strings.Notification_StarGift_Subtitle_Converted_Stars(Int32(convertStars))).string + } else if savedToProfile { + text = item.presentationData.strings.Notification_StarGift_Subtitle_Displaying(item.presentationData.strings.Notification_StarGift_Subtitle_Displaying_Stars(Int32(convertStars))).string + } else { + text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string + } + } else { + var peerName = "" + if let peer = item.message.peers[item.message.id.peerId] { + peerName = EnginePeer(peer).compactDisplayTitle + } + if peerName.isEmpty { + text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string + } else { + text = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars))).string + } + } + } + animationFile = gift.file + if let availability = gift.availability { + let availabilityString: String + if availability.total > 9999 { + availabilityString = compactNumericCountString(Int(availability.total)) + } else { + availabilityString = "\(availability.total)" + } + ribbonTitle = item.presentationData.strings.Notification_StarGift_OneOf(availabilityString).string + } + if incoming { + buttonTitle = item.presentationData.strings.Notification_StarGift_View + } else { + buttonTitle = "" + } default: break } @@ -335,20 +470,45 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), - linkAttribute: { url in - return ("URL", url) - } - ), textAlignment: .center) - - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let attributedText: NSAttributedString + if !entities.isEmpty { + attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil) + } else { + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + } + + let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (_, spoilerSubtitleApply) = makeSpoilerSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets(), displaySpoilers: true)) + + var canExpand = false + var clippedTextHeight: CGFloat = subtitleLayout.size.height + if subtitleLayout.numberOfLines > 4 { + let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + canExpand = true + if !currentIsExpanded { + clippedTextHeight = measuredTextLayout.size.height + } + } + let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - - giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0 + + let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0 + if !buttonTitle.isEmpty { + giftSize.height += 48.0 + } var labelRects = labelLayout.linesRects() if labelRects.count > 1 { @@ -384,7 +544,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { backgroundMaskImage = nil } - var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) + var backgroundSize = giftSize if hasServiceMessage { backgroundSize.height += labelLayout.size.height + 18.0 } else { @@ -392,67 +552,206 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } return (backgroundSize.width, { boundingWidth in - return (backgroundSize, { [weak self] animation, synchronousLoads, _ in + return (backgroundSize, { [weak self] animation, synchronousLoads, info in if let strongSelf = self { + let isFirstTime = strongSelf.item == nil + + if strongSelf.appliedIsExpanded != currentIsExpanded { + strongSelf.appliedIsExpanded = currentIsExpanded + info?.setInvertOffsetDirection() + + if let maskOverlayView = strongSelf.maskOverlayView { + animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0) + } + } + + let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) + + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) + let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) + + var iconSize = CGSize(width: 160.0, height: 160.0) + var iconOffset: CGFloat = 0.0 + if let _ = animationFile { + iconSize = CGSize(width: 120.0, height: 120.0) + iconOffset = 32.0 + } + let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) + strongSelf.animationNode.frame = animationFrame + + strongSelf.buttonNode.isHidden = buttonTitle.isEmpty + strongSelf.buttonTitleNode.isHidden = buttonTitle.isEmpty + if strongSelf.item == nil { + strongSelf.animationNode.started = { [weak self] in + if let strongSelf = self { + let current = CACurrentMediaTime() + if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 { + if !strongSelf.placeholderNode.alpha.isZero { + strongSelf.animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.removePlaceholder(animated: true) + } + } else { + strongSelf.removePlaceholder(animated: false) + } + } + } + strongSelf.animationNode.autoplay = true - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) + + if let file = animationFile { + strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + if strongSelf.fetchDisposable == nil { + strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start() + } + + if let immediateThumbnailData = file.immediateThumbnailData { + let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: overlayColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency) + } + } else if animationName.hasPrefix("Gift") { + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) + } } strongSelf.item = item - + strongSelf.isStarGift = isStarGift + strongSelf.updateVisibility() strongSelf.labelNode.isHidden = !hasServiceMessage - - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) - let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) - strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame - - strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) - strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate) - strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) - let iconSize = CGSize(width: 160.0, height: 160.0) - strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0), size: iconSize) + strongSelf.buttonNode.backgroundColor = overlayColor + strongSelf.animationNode.updateLayout(size: iconSize) + strongSelf.placeholderNode.frame = animationFrame let _ = labelApply() let _ = titleApply() - let _ = subtitleApply() + let _ = subtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) let _ = buttonTitleApply() - - let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) + let _ = ribbonTextApply() + let _ = moreApply() + + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) - strongSelf.subtitleNode.frame = subtitleFrame + let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) + + let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size) + strongSelf.subtitleNode.textNode.frame = subtitleFrame + + if isFirstTime { + strongSelf.textClippingNode.frame = clippingTextFrame + } else { + animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) + } + if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { + animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) + animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) + } + animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil) - let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) - strongSelf.buttonTitleNode.frame = buttonTitleFrame + if !subtitleLayout.spoilers.isEmpty { + let spoilerSubtitleNode = spoilerSubtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) + if strongSelf.spoilerSubtitleNode == nil { + spoilerSubtitleNode.textNode.alpha = 0.0 + spoilerSubtitleNode.textNode.isUserInteractionEnabled = false + strongSelf.spoilerSubtitleNode = spoilerSubtitleNode + + strongSelf.textClippingNode.addSubnode(spoilerSubtitleNode.textNode) + } + spoilerSubtitleNode.textNode.frame = subtitleFrame + + let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) + strongSelf.dustNode = dustNode + strongSelf.textClippingNode.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode) + } + dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0) + dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = strongSelf.dustNode { + dustNode.removeFromSupernode() + strongSelf.dustNode = nil + } let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) - strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) + strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - buttonTitleLayout.size.width) / 2.0), y: 8.0), size: buttonTitleLayout.size) + + animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize), completion: nil) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) - - if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { - strongSelf.mediaBackgroundNode.isHidden = true + + if ribbonTextLayout.size.width > 0.0 { + if strongSelf.ribbonBackgroundNode.image == nil { + let ribbonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/GiftRibbon"), color: overlayColor) + strongSelf.ribbonBackgroundNode.image = ribbonImage + } + if let ribbonImage = strongSelf.ribbonBackgroundNode.image { + let ribbonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.maxX - ribbonImage.size.width + 2.0, y: mediaBackgroundFrame.minY - 2.0), size: ribbonImage.size) + strongSelf.ribbonBackgroundNode.frame = ribbonFrame + + strongSelf.ribbonTextNode.transform = CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0) + strongSelf.ribbonTextNode.bounds = CGRect(origin: .zero, size: ribbonTextLayout.size) + strongSelf.ribbonTextNode.position = ribbonFrame.center.offsetBy(dx: 7.0, dy: -6.0) + } + } + + if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + backgroundContent.cornerRadius = 24.0 + + strongSelf.mediaBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.mediaBackgroundContent { + if ribbonTextLayout.size.width > 0.0 { + let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0) + backgroundContent.frame = backgroundMaskFrame + animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil) + backgroundContent.cornerRadius = 0.0 + + if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size { + strongSelf.mediaBackgroundMaskNode.image = generateImage(backgroundMaskFrame.size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.setFillColor(UIColor.black.cgColor) + context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 2.0, dy: 2.0), cornerRadius: 24.0).cgPath) + context.fillPath() + + if let ribbonImage = UIImage(bundleImageName: "Chat/Message/GiftRibbon"), let cgImage = ribbonImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: bounds.width - ribbonImage.size.width, y: bounds.height - ribbonImage.size.height), size: ribbonImage.size), byTiling: false) + } + }) + } + backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view + strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size) + } else { + animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) backgroundContent.clipsToBounds = true - backgroundContent.allowsGroupOpacity = true backgroundContent.cornerRadius = 24.0 - - strongSelf.mediaBackgroundContent = backgroundContent - strongSelf.insertSubnode(backgroundContent, at: 0) + backgroundContent.view.mask = nil } - - strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame - } else { - strongSelf.mediaBackgroundNode.isHidden = false - strongSelf.mediaBackgroundContent?.removeFromSupernode() - strongSelf.mediaBackgroundContent = nil } let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) @@ -487,6 +786,28 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } + + if canExpand, let maskView = strongSelf.maskView { + if maskView.image == nil { + maskView.image = generateMaskImage() + } + strongSelf.textClippingNode.view.mask = strongSelf.maskView + + animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil) + } else { + strongSelf.textClippingNode.view.mask = nil + strongSelf.moreTextNode.alpha = 0.0 + } + + switch strongSelf.visibility { + case .none: + strongSelf.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.subtitleNode.visibilityRect = subRect + } } }) }) @@ -579,8 +900,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.labelNode.frame - if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { + if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - self.labelNode.frame.minX, y: point.y - self.labelNode.frame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { @@ -598,11 +918,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } + if let (_, attributes) = self.subtitleNode.textNode.attributesAtPoint(CGPoint(x: point.x - self.textClippingNode.frame.minX, y: point.y - self.textClippingNode.frame.minY)), gesture == .tap { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], let dustNode = self.dustNode, !dustNode.isRevealed { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) + } else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero { + return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in + self?.expandPressed() + })) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) - } else if self.mediaBackgroundNode.frame.contains(point) { + } else if self.mediaBackgroundContent?.frame.contains(point) == true { return ChatMessageBubbleContentTapAction(content: .openMessage) } else { return ChatMessageBubbleContentTapAction(content: .none) @@ -613,6 +943,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.updateVisibility() } + private var internalPlayedOnce = false private func updateVisibility() { guard let item = self.item else { return @@ -643,9 +974,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { + if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) && !self.internalPlayedOnce { item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) self.animationNode.playOnce() + self.internalPlayedOnce = true Queue.mainQueue().after(0.05) { if let itemNode = self.itemNode, let supernode = itemNode.supernode { @@ -656,10 +988,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !alreadySeen && self.animationNode.isPlaying { item.controllerInteraction.playNextOutgoingGift = false - Queue.mainQueue().after(1.0) { + + Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) { item.controllerInteraction.animateDiceSuccess(false, true) } } } } } + +private func generateMaskImage() -> UIImage? { + return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.setBlendMode(.copy) + context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0))) + context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0)) +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 079ea26cf62..bfbbd9a7c4e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -312,7 +312,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } @objc private func progressPressed() { - if let resourceStatus = self.resourceStatus { + if let _ = self.arguments?.attributes.updatingMedia { + if let message = self.message { + self.context?.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } + } + else if let resourceStatus = self.resourceStatus { switch resourceStatus.mediaStatus { case let .fetchStatus(fetchStatus): if let context = self.context, let message = self.message, message.flags.isSending { @@ -724,10 +729,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusUpdated = true } - let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnailImage = !arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil + if case let .update(media) = arguments.attributes.updatingMedia?.media, let file = media.media as? TelegramMediaFile { + hasThumbnail = largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil || file.mimeType.hasPrefix("image/") + hasThumbnailImage = hasThumbnail + } if mediaUpdated { - if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { + if hasThumbnailImage { updateImageSignal = chatMessageImageFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } @@ -784,7 +794,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing let isInstantVideo = arguments.file.isInstantVideo for attribute in arguments.file.attributes { - if case let .Video(videoDuration, _, flags, _, _) = attribute, flags.contains(.instantRoundVideo) { + if case let .Video(videoDuration, _, flags, _, _, _) = attribute, flags.contains(.instantRoundVideo) { isAudio = true isVoice = true @@ -1706,7 +1716,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32? for attribute in file.attributes { - if case let .Video(duration, _, flags, _, _) = attribute, flags.contains(.instantRoundVideo) { + if case let .Video(duration, _, flags, _, _, _) = attribute, flags.contains(.instantRoundVideo) { isAudio = true isVoice = true audioDuration = Int32(duration) @@ -1770,59 +1780,64 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - switch resourceStatus.mediaStatus { - case var .fetchStatus(fetchStatus): - if self.message?.forwardInfo != nil { - fetchStatus = resourceStatus.fetchStatus - } - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - - switch fetchStatus { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - var wasCheck = false - if let statusNode = self.statusNode, case .check = statusNode.state { - wasCheck = true + if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media { + let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } else { + switch resourceStatus.mediaStatus { + case var .fetchStatus(fetchStatus): + if self.message?.forwardInfo != nil { + fetchStatus = resourceStatus.fetchStatus } + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - if isAudio && !isVoice && !isSending { - state = .play - } else { - if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { - state = .check(appearance: nil) + switch fetchStatus { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + var wasCheck = false + if let statusNode = self.statusNode, case .check = statusNode.state { + wasCheck = true + } + + if isAudio && !isVoice && !isSending { + state = .play } else { - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { + state = .check(appearance: nil) + } else { + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } + } + case .Local: + if isAudio { + state = .play + } else if let fileIconImage = self.fileIconImage { + state = .customIcon(fileIconImage) + } else { + state = .none + } + case .Remote, .Paused: + if isAudio && !isVoice { + state = .play + } else { + state = .download } } - case .Local: - if isAudio { - state = .play - } else if let fileIconImage = self.fileIconImage { - state = .customIcon(fileIconImage) - } else { - state = .none - } - case .Remote, .Paused: - if isAudio && !isVoice { - state = .play + case let .playbackStatus(playbackStatus): + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage + + if isViewOnceMessage && playbackStatus == .playing { + state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) + if incoming { + self.consumableContentNode.isHidden = true + } } else { - state = .download - } - } - case let .playbackStatus(playbackStatus): - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage - - if isViewOnceMessage && playbackStatus == .playing { - state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) - if incoming { - self.consumableContentNode.isHidden = true - } - } else { - switch playbackStatus { - case .playing: - state = .pause - case .paused: - state = .play + switch playbackStatus { + case .playing: + state = .pause + case .paused: + state = .play + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 5dcd3a1f815..9bd38d928d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -763,7 +763,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { }) } let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { if let strongSelf = self { if let item = strongSelf.item { if strongSelf.infoBackgroundNode.alpha.isZero { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 530fa155fab..f1f5d5c6094 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -414,8 +414,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } private func selectStoryMedia(item: Stories.Item, preferredHighQuality: Bool) -> Media? { - if !preferredHighQuality, let alternativeMedia = item.alternativeMedia { - return alternativeMedia + if !preferredHighQuality, let alternativeMediaValue = item.alternativeMediaList.first { + return alternativeMediaValue } else { return item.media } @@ -430,7 +430,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var highQualityImageNode: TransformImageNode? private var videoNode: UniversalVideoNode? - private var videoContent: NativeVideoContent? + private var videoContent: UniversalVideoContent? private var animatedStickerNode: AnimatedStickerNode? private var statusNode: RadialStatusNode? public var videoNodeDecoration: ChatBubbleVideoDecoration? @@ -513,6 +513,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public var updateMessageReaction: ((Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void)? public var playMessageEffect: ((Message) -> Void)? public var activateAgeRestrictedMedia: (() -> Void)? + public var requestInlineUpdate: (() -> Void)? + + private var hlsInlinePlaybackRange: Range? + private var appliedHlsInlinePlaybackRange: Range? + private var hlsInlinePlaybackRangeDisposable: Disposable? override public init() { self.pinchContainerNode = PinchSourceContainerNode() @@ -618,10 +623,15 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr self.playerStatusDisposable.dispose() self.fetchDisposable.dispose() self.secretTimer?.invalidate() + self.hlsInlinePlaybackRangeDisposable?.dispose() } public func isAvailableForGalleryTransition() -> Bool { - return self.automaticPlayback ?? false + if let automaticPlayback = self.automaticPlayback, automaticPlayback, self.decoration != nil { + return true + } else { + return false + } } public func isAvailableForInstantPageTransition() -> Bool { @@ -680,10 +690,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } - if let alternativeMedia = item.alternativeMedia { - if let media = alternativeMedia as? TelegramMediaFile { + if let alternativeMediaValue = item.alternativeMediaList.first { + if let media = alternativeMediaValue as? TelegramMediaFile { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media) - } else if let media = alternativeMedia as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { + } else if let media = alternativeMediaValue as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } } @@ -719,7 +729,20 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { var videoContentMatch = true - if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { + if let content = self.videoContent as? NativeVideoContent, case let .message(stableId, mediaId) = content.nativeId { + var media = self.media + if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + media = fullMedia + } + + if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let _ = item.media { + media = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality) + } + } + + videoContentMatch = self.message?.stableId == stableId && media?.id == mediaId + } else if let content = self.videoContent as? PlatformVideoContent, case let .message(_, stableId, mediaId) = content.nativeId { var media = self.media if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia @@ -770,6 +793,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback + let hlsInlinePlaybackRange = self.hlsInlinePlaybackRange + let appliedHlsInlinePlaybackRange = self.appliedHlsInlinePlaybackRange + return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in let _ = peerType @@ -1076,6 +1102,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else { mediaUpdated = true } + let inlinePlaybackRangeUpdated = hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange var isSendingUpdated = false if let currentMessage = currentMessage { @@ -1098,6 +1125,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var updateAnimatedStickerFile: TelegramMediaFile? var onlyFullSizeVideoThumbnail: Bool? + var loadHLSRangeVideoFile: TelegramMediaFile? + var emptyColor: UIColor var patternArguments: PatternWallpaperArguments? if isSticker { @@ -1131,7 +1160,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { + let reloadMedia = mediaUpdated || isSendingUpdated || automaticPlaybackUpdated + if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated || inlinePlaybackRangeUpdated { var media = media var extendedMedia: TelegramExtendedMedia? @@ -1207,7 +1237,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: resource) } }) - } else if let file = media as? TelegramMediaFile { + } else if var file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) @@ -1239,20 +1269,34 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { - updateVideoFile = file - if hasCurrentVideoNode { - if let currentFile = currentMedia as? TelegramMediaFile { - if currentFile.resource is EmptyMediaResource { - replaceVideoNode = true - } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { - replaceVideoNode = true - } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { - replaceVideoNode = true - } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { - replaceVideoNode = true + loadHLSRangeVideoFile = file + + var passFile = true + if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) { + file = minimizedQualityFile.file.media + if hlsInlinePlaybackRange == nil { + passFile = false + } + } + + if passFile { + updateVideoFile = file + if hasCurrentVideoNode { + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true + } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { + replaceVideoNode = true + } } + } else if !(file.resource is LocalFileVideoMediaResource) { + replaceVideoNode = true } - } else if !(file.resource is LocalFileVideoMediaResource) { + } else if hasCurrentVideoNode { replaceVideoNode = true } } else { @@ -1343,52 +1387,41 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr }, cancel: { chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image) }) - } else if let file = media as? TelegramMediaFile { - if isSecretMedia { - updateImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) - } - } else { - if file.isAnimatedSticker { - let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = { synchronousLoad, _ in - return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) - } - } else if file.isSticker || file.isVideoSticker { - updateImageSignal = { synchronousLoad, _ in - return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) - } - } else { - onlyFullSizeVideoThumbnail = isSendingUpdated - updateImageSignal = { synchronousLoad, _ in - return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) - } - updateBlurredImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) - } - } - } - + } else if var file = media as? TelegramMediaFile { var uploading = false if file.resource is VideoLibraryMediaResource { uploading = true } if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { - updateVideoFile = file - if hasCurrentVideoNode { - if let currentFile = currentMedia as? TelegramMediaFile { - if currentFile.resource is EmptyMediaResource { - replaceVideoNode = true - } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { - replaceVideoNode = true - } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { - replaceVideoNode = true - } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { - replaceVideoNode = true + loadHLSRangeVideoFile = file + + var passFile = true + if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) { + file = minimizedQualityFile.file.media + if hlsInlinePlaybackRange == nil { + passFile = false + } + } + + if passFile { + updateVideoFile = file + if hasCurrentVideoNode { + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true + } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { + replaceVideoNode = true + } } + } else if !(file.resource is LocalFileVideoMediaResource) { + replaceVideoNode = true } - } else if !(file.resource is LocalFileVideoMediaResource) { + } else if hasCurrentVideoNode { replaceVideoNode = true } } else { @@ -1412,10 +1445,37 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + if isSecretMedia { + updateImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) + } + } else { + if file.isAnimatedSticker { + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = { synchronousLoad, _ in + return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) + } + } else if file.isSticker || file.isVideoSticker { + updateImageSignal = { synchronousLoad, _ in + return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) + } + } else { + onlyFullSizeVideoThumbnail = isSendingUpdated + updateImageSignal = { synchronousLoad, _ in + return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) + } + } + } + updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).startStrict()) + } else if NativeVideoContent.isHLSVideo(file: file) { + strongSelf.fetchDisposable.set(nil) } else { // MARK: Nicegram downloading feature, shouldSave added strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, storeToDownloadsPeerId: storeToDownloadsPeerId, shouldSave: true).startStrict()) @@ -1479,6 +1539,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if !reloadMedia { + updateImageSignal = nil + } else { + print("reload media") + } var isExtendedMedia = false if statusUpdated { @@ -1649,13 +1714,30 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) let loopVideo = updatedVideoFile.isAnimated - let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in - guard let context, let peerId else { - return + + let videoContent: UniversalVideoContent + videoContent = NativeVideoContent( + id: .message(message.stableId, updatedVideoFile.fileId), + userLocation: .peer(message.id.peerId), + fileReference: .message(message: MessageReference(message), media: updatedVideoFile), + limitedFileRange: hlsInlinePlaybackRange, + streamVideo: streamVideo ? .conservative : .none, + loopVideo: loopVideo, + enableSound: false, + fetchAutomatically: false, + onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), + autoFetchFullSizeThumbnail: true, + continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, + placeholderColor: emptyColor, + captureProtected: message.isCopyProtected() || isExtendedMedia, + storeAfterDownload: { [weak context] in + guard let context, let peerId else { + return + } + let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() } - let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() - }) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + ) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { @@ -1673,6 +1755,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr videoNode.isHidden = true strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) } + //videoNode.alpha = 0.5 updatedVideoNodeReadySignal = videoNode.ready updatedPlayerStatusSignal = videoNode.status @@ -1836,7 +1919,32 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if case .full = automaticDownload { + if automaticDownload != .none, let file = media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) { + let postbox = context.account.postbox + let fetchSignal = HLSVideoContent.minimizedHLSQualityPreloadData(postbox: context.account.postbox, file: .message(message: MessageReference(message), media: file), userLocation: .peer(message.id.peerId), prefixSeconds: 10, autofetchPlaylist: true) + |> mapToSignal { fileAndRange -> Signal in + guard let fileAndRange else { + return .complete() + } + return freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: .peer(message.id.peerId), fileReference: fileAndRange.0, resource: fileAndRange.0.media.resource, range: (fileAndRange.1, .default)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } + + let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() + |> mapToSignal { visibility -> Signal in + if visibility { + return fetchSignal + |> mapToSignal { _ -> Signal in + } + } else { + return .complete() + } + } + strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.startStrict()) + } else if case .full = automaticDownload { if let _ = media as? TelegramMediaImage { updatedFetchControls.fetch(false) } else if let image = media as? TelegramMediaWebFile { @@ -1879,6 +1987,36 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.updateStatus(animated: synchronousLoads) strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia && !isExtendedMediaPreview && !hasSpoiler + + strongSelf.appliedHlsInlinePlaybackRange = hlsInlinePlaybackRange + + if let loadHLSRangeVideoFile, NativeVideoContent.isHLSVideo(file: loadHLSRangeVideoFile) { + if strongSelf.hlsInlinePlaybackRangeDisposable == nil { + strongSelf.hlsInlinePlaybackRangeDisposable = (HLSVideoContent.minimizedHLSQualityPreloadData( + postbox: context.account.postbox, + file: .message(message: MessageReference(message), media: loadHLSRangeVideoFile), + userLocation: .peer(message.id.peerId), + prefixSeconds: 10, + autofetchPlaylist: false + ) + //|> delay(2.0, queue: .mainQueue()) + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] preloadData in + guard let strongSelf else { + return + } + let hlsInlinePlaybackRange: Range? + if let preloadData { + hlsInlinePlaybackRange = preloadData.1 + } else { + hlsInlinePlaybackRange = nil + } + if strongSelf.hlsInlinePlaybackRange != hlsInlinePlaybackRange { + strongSelf.hlsInlinePlaybackRange = hlsInlinePlaybackRange + strongSelf.requestInlineUpdate?() + } + }) + } + } } }) }) @@ -2103,6 +2241,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + + if let file = self.media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) { + fetchStatus = .Local + } let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) @@ -2634,8 +2776,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { var isAnimated = false - if let file = self.media as? TelegramMediaFile, file.isAnimated { - isAnimated = true + if let file = self.media as? TelegramMediaFile { + if NativeVideoContent.isHLSVideo(file: file) { + return nil + } + if file.isAnimated { + isAnimated = true + } } var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD index 26c721aa8e7..07c49d0beec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode", "//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode", "//submodules/AvatarNode", "//submodules/TelegramUniversalVideoContent", "//submodules/MediaPlayer:UniversalMediaPlayer", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 5ec79904943..fd998f651e2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -110,7 +110,7 @@ private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String } private func dateHeaderTimestampId(timestamp: Int32) -> Int32 { - if timestamp == scheduleWhenOnlineTimestamp { + if timestamp == scheduleWhenOnlineTimestamp || timestamp >= Int32.max - 1000 { return timestamp } else if timestamp == Int32.max { return timestamp / (granularity) * (granularity) @@ -551,7 +551,11 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat } public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { - self.containerNode.isGestureEnabled = true + if let messageReference = self.messageReference, let id = messageReference.id { + self.containerNode.isGestureEnabled = !id.peerId.isVerificationCodes + } else { + self.containerNode.isGestureEnabled = true + } var overrideImage: AvatarNodeImageOverride? if peer.isDeleted { @@ -754,8 +758,10 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _, _) = self.messageReference?.content { self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, self, self.avatarNode.frame) } else if let peer = self.peer { - if let adMessageId = self.adMessageId { - self.controllerInteraction?.activateAdAction(adMessageId, nil) + if peer.id.isVerificationCodes { + self.controllerInteraction?.playShakeAnimation() + } else if let adMessageId = self.adMessageId { + self.controllerInteraction?.activateAdAction(adMessageId, nil, false, false) } else { if let channel = peer as? TelegramChannel, case .broadcast = channel.info { self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 0aeb544c623..357edfa9e88 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -27,7 +27,7 @@ private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { switch attribute { case .Sticker: return .semanticallyMerged - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return .none } @@ -305,6 +305,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) } } + if peerId.isVerificationCodes && effectiveAuthor == nil { + effectiveAuthor = content.firstMessage.author + } displayAuthorInfo = incoming && effectiveAuthor != nil } else { effectiveAuthor = content.firstMessage.author @@ -444,7 +447,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible viewClassName = ChatMessageStickerItemNode.self } break loop - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { viewClassName = ChatMessageBubbleItemNode.self break loop diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index ea4dc7c4800..d81990743d4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -203,7 +203,7 @@ public final class ChatMessageAccessibilityData { text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).string text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string) } - case let .Video(duration, _, flags, _, _): + case let .Video(duration, _, flags, _, _, _): isSpecialFile = true if isSelected == nil { hint = item.presentationData.strings.VoiceOver_Chat_PlayHint @@ -867,6 +867,8 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { item.controllerInteraction.openWebView(button.title, url, simple, .generic) case .requestPeer: break + case let .copyText(payload): + item.controllerInteraction.copyText(payload) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD index 9361bb92768..b2fd8cff875 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + "//submodules/TelegramUniversalVideoContent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index dbe1880c410..e6e5362a0ce 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -16,6 +16,7 @@ import ChatMessageItemCommon import ChatMessageInteractiveMediaNode import ChatControllerInteraction import InvisibleInkDustNode +import TelegramUniversalVideoContent public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override public var supportsMosaic: Bool { @@ -88,6 +89,12 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.item?.controllerInteraction.playMessageEffect(message) } + self.interactiveImageNode.requestInlineUpdate = { [weak self] in + guard let self else { + return + } + self.requestInlineUpdate?() + } } required public init?(coder aDecoder: NSCoder) { @@ -163,7 +170,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { - if case .full = automaticDownload { + if NativeVideoContent.isHLSVideo(file: telegramFile) { + automaticPlayback = true + } else if case .full = automaticDownload { automaticPlayback = true } else { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil @@ -207,7 +216,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { - if case .full = automaticDownload { + if NativeVideoContent.isHLSVideo(file: telegramFile) { + automaticPlayback = true + } else if case .full = automaticDownload { automaticPlayback = true } else { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil @@ -555,4 +566,11 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } return nil } + + override public func getStatusNode() -> ASDisplayNode? { + if !self.interactiveImageNode.dateAndStatusNode.isHidden { + return self.interactiveImageNode.dateAndStatusNode + } + return nil + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/BUILD new file mode 100644 index 00000000000..21bc4c974bc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +# todo: cleanup +swift_library( + name = "ChatMessageNicegramAdNode", + module_name = "ChatMessageNicegramAdNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "@swiftpkg_nicegram_assistant_ios//:FeatAttentionEconomy", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdItem.swift new file mode 100644 index 00000000000..a616db2ecce --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdItem.swift @@ -0,0 +1,72 @@ +import FeatAttentionEconomy + +import AccountContext +import ChatControllerInteraction +import Display +import Foundation +import SwiftSignalKit +import TelegramPresentationData + +@available(iOS 15.0, *) +public final class ChatMessageNicegramAdItem: ListViewItem { + public let ad: AttAd + public let chatLocation: ChatLocation + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let presentationData: ChatPresentationData + + public init(ad: AttAd, chatLocation: ChatLocation, context: AccountContext, controllerInteraction: ChatControllerInteraction, presentationData: ChatPresentationData) { + self.ad = ad + self.chatLocation = chatLocation + self.context = context + self.controllerInteraction = controllerInteraction + self.presentationData = presentationData + } + + public func nodeConfiguredForParams( + async: @escaping (@escaping () -> Void) -> Void, + params: ListViewItemLayoutParams, + synchronousLoads: Bool, + previousItem: ListViewItem?, + nextItem: ListViewItem?, + completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void + ) { + let configure = { () -> Void in + let node = ChatMessageNicegramAdNode(rotated: self.controllerInteraction.chatIsRotated) + node.setupItem(self) + + let nodeLayout = node.asyncLayout() + let (layout, apply) = nodeLayout(self, params, false, false, false) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + if Thread.isMainThread { + configure() + } else { + Queue.mainQueue().async(configure) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ChatMessageNicegramAdNode { + nodeValue.setupItem(self) + + let nodeLayout = nodeValue.asyncLayout() + + let (layout, apply) = nodeLayout(self, params, false, false, false) + + completion(layout, { _ in + apply(animation) + }) + } else { + assertionFailure() + } + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdNode.swift new file mode 100644 index 00000000000..da876ec2663 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNicegramAdNode/Sources/ChatMessageNicegramAdNode.swift @@ -0,0 +1,163 @@ +import FeatAttentionEconomy + +import AsyncDisplayKit +import ChatMessageItemCommon +import ChatMessageItemView +import Display +import ShareController + +@available(iOS 15.0, *) +class ChatMessageNicegramAdNode: ListViewItemNode { + private let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular) + + var item: ChatMessageNicegramAdItem? + + private let bannerView: AttChatBanner + private let bannerNode: ASDisplayNode + + override var visibility: ListViewItemNodeVisibility { + didSet { + let visiblePart: Double + switch visibility { + case .none: + visiblePart = 0.0 + case let .visible(part, _): + visiblePart = part + } + + bannerView.set(visiblePart: visiblePart) + } + } + + required init(rotated: Bool) { + let bannerView = AttChatBanner() + self.bannerView = bannerView + self.bannerNode = ASDisplayNode { + bannerView + } + + super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) + + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } + + self.addSubnode(bannerNode) + + bannerView.share = { @MainActor [weak self] image, text in + guard let item = self?.item else { return } + let shareController = await shareController( + image: image, + text: text, + context: item.context + ) + item.controllerInteraction.presentController(shareController, nil) + } + } + + func setupItem(_ item: ChatMessageNicegramAdItem) { + self.item = item + + let chatId = item.chatLocation.peerId?.ng_toInt64() + self.bannerView.set(chatId: chatId) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + + } + + func asyncLayout() -> (_ item: ChatMessageNicegramAdItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in + guard let self else { + return ( + ListViewItemNodeLayout( + contentSize: .zero, + insets: .zero + ), + { _ in } + ) + } + + let presentationData = item.presentationData + let messagePresentationData = item.presentationData.theme.theme.chat.message + let incomingBubble = if presentationData.theme.wallpaper.hasWallpaper { + messagePresentationData.incoming.bubble.withWallpaper + } else { + messagePresentationData.incoming.bubble.withoutWallpaper + } + let bannerPresentationData = AttChatBannerPresentationData( + incomingBubble: .init( + backgroundColor: incomingBubble.fill.first ?? .black, + primaryTextColor: messagePresentationData.incoming.primaryTextColor + ), + messageFont: presentationData.messageFont, + messageBoldFont: presentationData.messageBoldFont + ) + + let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: presentationData) + let maximumWidthFill = layoutConstants.bubble.maximumWidthFill.widthFor(params.width) + let layoutParams = AttChatBannerLayoutParams( + insets: layoutConstants.bubble.contentInsets + .sum(.vertical(layoutConstants.bubble.defaultSpacing)) + .sum(.horizontal(layoutConstants.bubble.edgeInset)) + .sum(.left(params.leftInset).right(params.rightInset)), + maximumWidthFill: maximumWidthFill + ) + + bannerView.set( + ad: item.ad, + layoutParams: layoutParams, + presentationData: bannerPresentationData + ) + bannerView.layoutIfNeeded() + let bannerSize = bannerView.systemLayoutSizeFitting( + UIView.layoutFittingExpandedSize + ) + let size = CGSize( + width: params.width, + height: bannerSize.height + ) + + let layout = ListViewItemNodeLayout( + contentSize: size, + insets: .zero + ) + + let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in + guard let self else { return } + bannerNode.frame = CGRect(origin: .zero, size: size) + } + + return (layout, apply) + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +private extension UIEdgeInsets { + func sum(_ other: UIEdgeInsets) -> UIEdgeInsets { + UIEdgeInsets( + top: self.top + other.top, + left: self.left + other.left, + bottom: self.bottom + other.bottom, + right: self.right + other.right + ) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift index 1d59686554f..af8e21f43d6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift @@ -167,7 +167,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } var avatarPeer = peer - if firstMessage.id.peerId.isReplies, let author = firstMessage.forwardInfo?.author { + if firstMessage.id.peerId.isRepliesOrVerificationCodes, let author = firstMessage.forwardInfo?.author { avatarPeer = EnginePeer(author) } self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: avatarPeer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 7f8236c6ee0..cbcbc37fc40 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -218,11 +218,11 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont } if let photo = photo, let video = photo.videoRepresentations.last, let id = photo.id?.id { - let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD index 85588cbecc7..71ceaf2fc95 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 5a77f740bd1..b5764dfd46d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -101,6 +101,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + private var forceStopAnimations: Bool = false + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() @@ -2161,6 +2163,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if !item.controllerInteraction.canReadHistory { isPlaying = false } + if self.forceStopAnimations { + isPlaying = false + } if !isPlaying { self.removeEffectAnimations() @@ -2192,6 +2197,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + override public func updateStickerSettings(forceStopAnimations: Bool) { + self.forceStopAnimations = forceStopAnimations + self.updateVisibility() + } + override public func messageEffectTargetView() -> UIView? { if let result = self.dateAndStatusNode.messageEffectTargetView() { return result diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index d41a6440f99..2c88943fafa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1376,7 +1376,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected() + let enableCopy = (!item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected()) || item.message.id.peerId.isVerificationCodes textSelectionNode.enableCopy = enableCopy var enableQuote = !item.message.text.isEmpty @@ -1390,7 +1390,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if !item.controllerInteraction.canSendMessages() && !enableCopy { enableQuote = false } - if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat { + if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat || item.message.id.peerId.isVerificationCodes { enableQuote = false } if item.message.containsSecretMedia { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD index a031cda0e19..3f2baa8b58d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 49875649c4b..ba57a1641a2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -118,7 +118,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent self.contentNode.activateAction = { [weak self] in if let strongSelf = self, let item = strongSelf.item { if let _ = item.message.adAttribute { - item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress()) + item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress(), false, false) } else { var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 7512ef03f89..9cef08d2d24 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -2286,7 +2286,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { } } else { let videoContent = NativeVideoContent(id: .message(message.stableId, video.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: video), streamVideo: .conservative, loopVideo: true, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: self.isStatic, continuePlayingWithoutSoundOnLostAudioSession: true, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) self.videoStatusDisposable.set((videoNode.status |> deliverOnMainQueue).startStrict(next: { [weak self] status in @@ -2477,10 +2477,15 @@ private func renderVideo(context: AccountContext, backgroundImage: UIImage, user let layerInstruction = compositionLayerInstruction(for: compositionTrack, assetTrack: assetTrack) instruction.layerInstructions = [layerInstruction] - guard let export = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else { + guard let exportValue = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else { completion(nil) return } + #if compiler(>=6.0) // Xcode 16 + nonisolated(unsafe) let export = exportValue + #else + let export = exportValue + #endif let videoName = UUID().uuidString let exportURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(videoName).appendingPathExtension("mp4") diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 6c39fe96db9..b5e99792de6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -616,7 +616,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -637,6 +639,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction @@ -1208,8 +1211,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let invoice { let inputData = Promise() inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in + |> map(Optional.init) + |> `catch` { _ -> Signal in return .single(nil) }) strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in @@ -1339,7 +1342,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { case let .localization(identifier): strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil) case .proxy, .confirmationCode, .cancelAccountReset, .share: - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, _ in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { peer, _ in if let strongSelf = self { strongSelf.openPeer(peer: peer) } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/BUILD index b0c0658e4cc..d1b28f4504c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 714ce82b874..ce99d5b1f8a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -257,7 +257,7 @@ public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageCo public func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: self.waveform.makeBitstream())] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [voiceMedia], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) @@ -473,7 +473,9 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -493,6 +495,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD index 1cbadf2f427..a314804803d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD index 15cf263a28f..dd49fc1d2cd 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index cc47b94b4ef..cc81c88cc45 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -256,7 +256,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void public let openJoinLink: (String) -> Void public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void - public let activateAdAction: (EngineMessage.Id, Promise?) -> Void + public let activateAdAction: (EngineMessage.Id, Promise?, Bool, Bool) -> Void + public let adContextAction: (Message, ASDisplayNode, ContextGesture?) -> Void + public let removeAd: (Data) -> Void public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void public let saveMediaToFiles: (EngineMessage.Id) -> Void public let openNoAdsDemo: () -> Void @@ -277,6 +279,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let navigateToStory: (Message, StoryId) -> Void public let attemptedNavigationToPrivateQuote: (Peer?) -> Void public let forceUpdateWarpContents: () -> Void + public let playShakeAnimation: () -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -389,7 +392,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void, openJoinLink: @escaping (String) -> Void, openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void, - activateAdAction: @escaping (EngineMessage.Id, Promise?) -> Void, + activateAdAction: @escaping (EngineMessage.Id, Promise?, Bool, Bool) -> Void, + adContextAction: @escaping (Message, ASDisplayNode, ContextGesture?) -> Void, + removeAd: @escaping (Data) -> Void, openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void, saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, @@ -409,6 +414,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol navigateToStory: @escaping (Message, StoryId) -> Void, attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, forceUpdateWarpContents: @escaping () -> Void, + playShakeAnimation: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -502,6 +508,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openJoinLink = openJoinLink self.openWebView = openWebView self.activateAdAction = activateAdAction + self.adContextAction = adContextAction + self.removeAd = removeAd self.openRequestedPeerSelection = openRequestedPeerSelection self.saveMediaToFiles = saveMediaToFiles self.openNoAdsDemo = openNoAdsDemo @@ -522,6 +530,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.navigateToStory = navigateToStory self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote self.forceUpdateWarpContents = forceUpdateWarpContents + self.playShakeAnimation = playShakeAnimation self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 00d54572675..cc25503ae52 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -170,6 +170,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hasStickers: Bool = true, hasGifs: Bool = true, hideBackground: Bool = false, + forceHasPremium: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)? ) -> Signal { let animationCache = context.animationCache @@ -187,6 +188,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, + forceHasPremium: forceHasPremium, hideBackground: hideBackground ) @@ -471,7 +473,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } // MARK: Nicegram OpenGifsShortcut, defaultTab param added - public init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { + public init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false) { // MARK: Nicegram OpenGifsShortcut self.defaultTab = defaultTab // @@ -695,7 +697,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") { + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") && !forceHasPremium { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index cfd2961cb85..5d07518d220 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -299,7 +299,11 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } } } - isEnabled = isEnabledValue + if peerView.peerId.isVerificationCodes { + isEnabled = false + } else { + isEnabled = isEnabledValue + } } case let .replyThread(type, count): let textFont = titleFont @@ -435,7 +439,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { switch titleContent { case let .peer(peerView, _, _, isScheduledMessages, _, _, _): if let peer = peerView.peer { - if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { + if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { inputActivitiesAllowed = false } } @@ -540,7 +544,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { state = .info(string, .generic) } else if let peer = peerView.peer { let servicePeer = isServicePeer(peer) - if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { + if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let user = peer as? TelegramUser { diff --git a/submodules/TelegramUI/Components/ContentReportScreen/BUILD b/submodules/TelegramUI/Components/ContentReportScreen/BUILD new file mode 100644 index 00000000000..6945d5411f2 --- /dev/null +++ b/submodules/TelegramUI/Components/ContentReportScreen/BUILD @@ -0,0 +1,41 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ContentReportScreen", + module_name = "ContentReportScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift new file mode 100644 index 00000000000..5585d5c5d75 --- /dev/null +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -0,0 +1,769 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import ListSectionComponent +import ListActionItemComponent +import NavigationStackComponent +import ItemListUI +import UndoUI +import AccountContext +import LottieComponent +import TextFieldComponent +import ListMultilineTextFieldItemComponent +import ButtonComponent + +private enum ReportResult { + case reported + case requestedMessageSelection +} + +private final class SheetPageContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + enum Content: Equatable { + struct Item: Equatable { + let title: String + let option: Data + } + + case options(items: [Item]) + case comment(isOptional: Bool, option: Data) + } + + let context: AccountContext + let isFirst: Bool + let title: String? + let subtitle: String + let content: Content + let action: (Content.Item, String?) -> Void + let pop: () -> Void + + init( + context: AccountContext, + isFirst: Bool, + title: String?, + subtitle: String, + content: Content, + action: @escaping (Content.Item, String?) -> Void, + pop: @escaping () -> Void + ) { + self.context = context + self.isFirst = isFirst + self.title = title + self.subtitle = subtitle + self.content = content + self.action = action + self.pop = pop + } + + static func ==(lhs: SheetPageContent, rhs: SheetPageContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class State: ComponentState { + var backArrowImage: (UIImage, PresentationTheme)? + + let playOnce = ActionSlot() + private var didPlayAnimation = false + + let textInputState = ListMultilineTextFieldItemComponent.ExternalState() + + func playAnimationIfNeeded() { + guard !self.didPlayAnimation else { + return + } + self.didPlayAnimation = true + self.playOnce.invoke(Void()) + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let back = Child(Button.self) + let title = Child(Text.self) + let animation = Child(LottieComponent.self) + let section = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + let textInputTag = NSObject() + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.modalBlocksBackgroundColor, cornerRadius: 8.0), + availableSize: CGSize(width: context.availableSize.width, height: 1000.0), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let backArrowImage: UIImage + if let (cached, cachedTheme) = state.backArrowImage, cachedTheme === theme { + backArrowImage = cached + } else { + backArrowImage = NavigationBarTheme.generateBackArrowImage(color: theme.list.itemAccentColor)! + state.backArrowImage = (backArrowImage, theme) + } + + let backContents: AnyComponent + if component.isFirst { + backContents = AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.list.itemAccentColor)) + } else { + backContents = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "arrow", component: AnyComponent(Image(image: backArrowImage, contentMode: .center))), + AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: strings.Common_Back, font: Font.regular(17.0), color: theme.list.itemAccentColor))) + ], spacing: 6.0) + ) + } + let back = back.update( + component: Button( + content: backContents, + action: { + component.pop() + } + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + context.add(back + .position(CGPoint(x: sideInset + back.size.width / 2.0 - (!component.isFirst ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0 + + let titleString: String + if let title = component.title { + titleString = title + } else { + titleString = "" + } + + let title = title.update( + component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 24.0 + + var items: [AnyComponentWithIdentity] = [] + var footer: AnyComponent? + + switch component.content { + case let .options(options): + for item in options { + items.append(AnyComponentWithIdentity(id: item.title, component: AnyComponent(ListActionItemComponent( + theme: theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: item.title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .arrow, + action: { _ in + component.action(item, nil) + } + )))) + } + case let .comment(isOptional, _): + contentSize.height -= 11.0 + + let animationHeight: CGFloat = 120.0 + let animation = animation.update( + component: LottieComponent( + content: LottieComponent.AppBundleContent(name: "Cop"), + startingPosition: .begin, + playOnce: state.playOnce + ), + environment: {}, + availableSize: CGSize(width: animationHeight, height: animationHeight), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0)) + ) + contentSize.height += animation.size.height + contentSize.height += 18.0 + + items.append( + AnyComponentWithIdentity(id: items.count, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: state.textInputState, + context: component.context, + theme: theme, + strings: strings, + initialText: "", + resetText: nil, + placeholder: isOptional ? strings.Report_Comment_Placeholder_Optional : strings.Report_Comment_Placeholder, + autocapitalizationType: .none, + autocorrectionType: .no, + returnKeyType: .done, + characterLimit: 140, + displayCharacterLimit: true, + emptyLineHandling: .notAllowed, + updated: { [weak state] _ in + state?.updated() + }, + returnKeyAction: { +// guard let self else { +// return +// } +// if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { +// titleView.endEditing(true) +// } + }, + textUpdateTransition: .spring(duration: 0.4), + tag: textInputTag + ))) + ) + + footer = AnyComponent(MultilineTextComponent( + text: .plain( + NSAttributedString(string: strings.Report_Comment_Info, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor) + ), + maximumNumberOfLines: 0 + )) + } + + let section = section.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.subtitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: footer, + items: items, + isModal: true + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(section + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0)) + ) + contentSize.height += section.size.height + contentSize.height += 54.0 + + if case let .comment(isOptional, option) = component.content { + contentSize.height -= 16.0 + + let action = component.action + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: strings.Report_Send, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor))), + isEnabled: isOptional || state.textInputState.hasText, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { + action(SheetPageContent.Content.Item(title: "", option: option), state.textInputState.text.string) + } + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 16.0 + + if environment.inputHeight.isZero && environment.safeInsets.bottom > 0.0 { + contentSize.height += environment.safeInsets.bottom + } + } + + contentSize.height += environment.inputHeight + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: ReportContentSubject + let title: String + let options: [ReportContentResult.Option] + let pts: Int + let openMore: () -> Void + let complete: (ReportResult) -> Void + let dismiss: () -> Void + let update: (ComponentTransition) -> Void + let requestSelectMessages: ((String, Data, String?) -> Void)? + + init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + pts: Int, + openMore: @escaping () -> Void, + complete: @escaping (ReportResult) -> Void, + dismiss: @escaping () -> Void, + update: @escaping (ComponentTransition) -> Void, + requestSelectMessages: ((String, Data, String?) -> Void)? + ) { + self.context = context + self.subject = subject + self.title = title + self.options = options + self.pts = pts + self.openMore = openMore + self.complete = complete + self.dismiss = dismiss + self.update = update + self.requestSelectMessages = requestSelectMessages + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.pts != rhs.pts { + return false + } + return true + } + + final class State: ComponentState { + var pushedOptions: [(title: String, subtitle: String, content: SheetPageContent.Content)] = [] + let disposable = MetaDisposable() + + var peer: EnginePeer? + private var peerDisposable: Disposable? + + init(context: AccountContext, subject: ReportContentSubject) { + super.init() + + self.peerDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: subject.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + self?.peer = peer + self?.updated() + }) + } + + deinit { + self.disposable.dispose() + self.peerDisposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let navigation = Child(NavigationStackComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + let update = component.update + + let accountContext = component.context + let subject = component.subject + let complete = component.complete + let requestSelectMessages = component.requestSelectMessages + let action: (SheetPageContent.Content.Item, String?) -> Void = { [weak state] item, message in + guard let state else { + return + } + state.disposable.set( + (accountContext.engine.messages.reportContent(subject: subject, option: item.option, message: message) + |> deliverOnMainQueue).start(next: { [weak state] result in + switch result { + case let .options(title, options): + state?.pushedOptions.append((item.title, title, .options(items: options.map { SheetPageContent.Content.Item(title: $0.text, option: $0.option) }))) + state?.updated(transition: .spring(duration: 0.45)) + case let .addComment(isOptional, option): + state?.pushedOptions.append((item.title, "", .comment(isOptional: isOptional, option: option))) + state?.updated(transition: .spring(duration: 0.45)) + case .reported: + complete(.reported) + } + }, error: { error in + if case .messageIdRequired = error { + requestSelectMessages?(item.title, item.option, message) + complete(.requestedMessageSelection) + } + }) + ) + } + + let mainTitle: String + switch component.subject { + case .peer: + if let peer = state.peer { + if case let .user(user) = peer { + if let _ = user.botInfo { + mainTitle = environment.strings.Report_Title_Bot + } else { + mainTitle = environment.strings.Report_Title_User + } + } else if case let .channel(channel) = peer, case .broadcast = channel.info { + mainTitle = environment.strings.Report_Title_Channel + } else { + mainTitle = environment.strings.Report_Title_Group + } + } else { + mainTitle = "" + } + case .messages: + mainTitle = environment.strings.Report_Title_Message + case .stories: + mainTitle = environment.strings.Report_Title_Story + } + + var items: [AnyComponentWithIdentity] = [] + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + SheetPageContent( + context: component.context, + isFirst: true, + title: mainTitle, + subtitle: component.title, + content: .options(items: component.options.map { + SheetPageContent.Content.Item(title: $0.text, option: $0.option) + }), + action: { item, message in + action(item, message) + }, + pop: { + component.dismiss() + } + ) + ))) + for pushedOption in state.pushedOptions { + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + SheetPageContent( + context: component.context, + isFirst: false, + title: pushedOption.title, + subtitle: pushedOption.subtitle, + content: pushedOption.content, + action: { item, message in + action(item, message) + }, + pop: { [weak state] in + state?.pushedOptions.removeLast() + update(.spring(duration: 0.45)) + } + ) + ))) + } + + var contentSize = CGSize(width: context.availableSize.width, height: 0.0) + let navigation = navigation.update( + component: NavigationStackComponent( + items: items, + clipContent: false, + requestPop: { [weak state] in + state?.pushedOptions.removeLast() + update(.spring(duration: 0.45)) + } + ), + environment: { environment }, + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: context.transition + ) + context.add(navigation + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigation.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(8.0) + ) + contentSize.height += navigation.size.height + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: ReportContentSubject + let title: String + let options: [ReportContentResult.Option] + let openMore: () -> Void + let complete: (ReportResult) -> Void + let requestSelectMessages: ((String, Data, String?) -> Void)? + + init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + openMore: @escaping () -> Void, + complete: @escaping (ReportResult) -> Void, + requestSelectMessages: ((String, Data, String?) -> Void)? + ) { + self.context = context + self.subject = subject + self.title = title + self.options = options + self.openMore = openMore + self.complete = complete + self.requestSelectMessages = requestSelectMessages + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.options != rhs.options { + return false + } + return true + } + + final class State: ComponentState { + var pts: Int = 0 + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let state = context.state + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + subject: context.component.subject, + title: context.component.title, + options: context.component.options, + pts: state.pts, + openMore: context.component.openMore, + complete: context.component.complete, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + }, + update: { [weak state] transition in + state?.pts += 1 + state?.updated(transition: transition) + }, + requestSelectMessages: context.component.requestSelectMessages + )), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + + +public final class ContentReportScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + forceDark: Bool = false, + completed: @escaping () -> Void, + requestSelectMessages: ((String, Data, String?) -> Void)? + ) { + self.context = context + + var completeImpl: ((ReportResult) -> Void)? + super.init( + context: context, + component: SheetContainerComponent( + context: context, + subject: subject, + title: title, + options: options, + openMore: {}, + complete: { hidden in + completeImpl?(hidden) + }, + requestSelectMessages: requestSelectMessages + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + completeImpl = { [weak self] result in + guard let self else { + return + } + let navigationController = self.navigationController + self.dismissAnimated() + + switch result { + case .reported: + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + Queue.mainQueue().after(0.4, { + completed() + + (navigationController?.viewControllers.last as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return true }), in: .current) + }) + case .requestedMessageSelection: + break + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 4d91e53e12d..84a6c225094 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -144,6 +144,34 @@ public func animationCacheFetchFile(postbox: Postbox, userLocation: MediaResourc } } +public func animationCacheLoadLocalFile(name: String, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { + return { options in + let source = AnimatedStickerNodeLocalFileSource(name: name) + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + switch type { + case .video: + cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) + case .lottie: + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + options.writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) + case .still: + cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor) + } + }) + + return ActionDisposable { + dataDisposable.dispose() + } + } +} + private func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { return generateImage(bounds, rotatedContext: { contextSize, context in let bounds = CGRect(origin: CGPoint(), size: contextSize) @@ -310,6 +338,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private var didProcessTintColor: Bool = false public private(set) var file: TelegramMediaFile? + private var localAnimationName: String? + private var infoDisposable: Disposable? private var disposable: Disposable? private var fetchDisposable: Disposable? @@ -440,6 +470,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } case .ton: self.updateTon() + case let .animation(name): + self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad) } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) @@ -629,6 +661,42 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = tonImage?.cgImage } + private func updateLocalAnimation(name: String, attemptSynchronousLoad: Bool) { + guard let arguments = self.arguments else { + return + } + + self.localAnimationName = name + + if attemptSynchronousLoad { + if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) { + + } + + self.loadAnimation() + } else { + self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: true, customColor: nil), completion: { [weak self] result, isFinal in + guard let strongSelf = self else { + return + } + strongSelf.loadAnimation() + }) + } + } + + private func loadLocalAnimation() { + guard let arguments = self.arguments else { + return + } + + guard let name = self.localAnimationName else { + return + } + + let keyframeOnly = arguments.pixelSize.width >= 120.0 + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil)) + } + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index a3a42cc62c2..512b446dcc1 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1534,6 +1534,8 @@ public extension EmojiPagerContentComponent { displaySearchWithPlaceholder = strings.Common_Search } else if case .stickerAlt = subject { displaySearchWithPlaceholder = strings.Common_Search + } else if case .reactionList = subject { + displaySearchWithPlaceholder = strings.Common_Search } } diff --git a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift index e133ecbacbf..5178c9659e7 100644 --- a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift +++ b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift @@ -126,7 +126,7 @@ public func paneGifSearchForQuery(context: AccountContext, query: String, offset )) } } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD new file mode 100644 index 00000000000..41a451a0c23 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftAnimationComponent", + module_name = "GiftAnimationComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift new file mode 100644 index 00000000000..c5d161e3de0 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift @@ -0,0 +1,98 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import AppBundle +import AccountContext +import EmojiTextAttachmentView +import TextFormat + +public final class GiftAnimationComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let file: TelegramMediaFile? + + public init( + context: AccountContext, + theme: PresentationTheme, + file: TelegramMediaFile? + ) { + self.context = context + self.theme = theme + self.file = file + } + + public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + public final class View: UIView { + private var component: GiftAnimationComponent? + private weak var componentState: EmptyComponentState? + + private var animationLayer: InlineStickerItemLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: component.file?.fileId.id ?? 0, + file: component.file + ) + + let iconSize = availableSize + if self.animationLayer == nil { + let animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: component.file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: true, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + } + if let animationLayer = self.animationLayer { + transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize)) + } + + return iconSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD new file mode 100644 index 00000000000..5a5b517e012 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftItemComponent", + module_name = "GiftItemComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/TextFormat", + "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift new file mode 100644 index 00000000000..1ed7108d39d --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -0,0 +1,648 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import AppBundle +import AccountContext +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import EmojiTextAttachmentView +import TextFormat +import ItemShimmeringLoadingComponent +import AvatarNode + +public final class GiftItemComponent: Component { + public enum Subject: Equatable { + case premium(Int32) + case starGift(Int64, TelegramMediaFile) + } + + public struct Ribbon: Equatable { + public enum Color { + case red + case blue + + func colors(theme: PresentationTheme) -> [UIColor] { + switch self { + case .red: + if theme.overallDarkAppearance { + return [ + UIColor(rgb: 0x522124), + UIColor(rgb: 0x653634) + + ] + } else { + return [ + UIColor(rgb: 0xed1c26), + UIColor(rgb: 0xff5c55) + + ] + } + case .blue: + if theme.overallDarkAppearance { + return [ + UIColor(rgb: 0x142e42), + UIColor(rgb: 0x354f5b) + ] + } else { + return [ + UIColor(rgb: 0x34a4fc), + UIColor(rgb: 0x6fd3ff) + ] + } + } + } + } + public let text: String + public let color: Color + + public init(text: String, color: Color) { + self.text = text + self.color = color + } + } + + public enum Peer: Equatable { + case peer(EnginePeer) + case anonymous + } + + let context: AccountContext + let theme: PresentationTheme + let peer: GiftItemComponent.Peer? + let subject: GiftItemComponent.Subject + let title: String? + let subtitle: String? + let price: String + let ribbon: Ribbon? + let isLoading: Bool + let isHidden: Bool + let isSoldOut: Bool + + public init( + context: AccountContext, + theme: PresentationTheme, + peer: GiftItemComponent.Peer?, + subject: GiftItemComponent.Subject, + title: String? = nil, + subtitle: String? = nil, + price: String, + ribbon: Ribbon? = nil, + isLoading: Bool = false, + isHidden: Bool = false, + isSoldOut: Bool = false + ) { + self.context = context + self.theme = theme + self.peer = peer + self.subject = subject + self.title = title + self.subtitle = subtitle + self.price = price + self.ribbon = ribbon + self.isLoading = isLoading + self.isHidden = isHidden + self.isSoldOut = isSoldOut + } + + public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.price != rhs.price { + return false + } + if lhs.ribbon != rhs.ribbon { + return false + } + if lhs.isLoading != rhs.isLoading { + return false + } + if lhs.isHidden != rhs.isHidden { + return false + } + if lhs.isSoldOut != rhs.isSoldOut { + return false + } + return true + } + + public final class View: UIView { + private var component: GiftItemComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private var loadingBackground: ComponentView? + + private var avatarNode: AvatarNode? + private let title = ComponentView() + private let subtitle = ComponentView() + private let button = ComponentView() + private let ribbon = UIImageView() + private let ribbonText = ComponentView() + + private var animationLayer: InlineStickerItemLayer? + + private var hiddenIconBackground: UIVisualEffectView? + private var hiddenIcon: UIImageView? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + + self.backgroundLayer.cornerRadius = 10.0 + if #available(iOS 13.0, *) { + self.backgroundLayer.cornerCurve = .circular + } + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isFirstTime = self.component == nil + let previousComponent = self.component + self.component = component + self.componentState = state + + var themeUpdated = false + if previousComponent?.theme !== component.theme { + themeUpdated = true + } + + let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0) + + if component.isLoading { + let loadingBackground: ComponentView + if let current = self.loadingBackground { + loadingBackground = current + } else { + loadingBackground = ComponentView() + self.loadingBackground = loadingBackground + } + + let _ = loadingBackground.update( + transition: transition, + component: AnyComponent( + ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: 10.0) + ), + environment: {}, + containerSize: size + ) + if let loadingBackgroundView = loadingBackground.view { + if loadingBackgroundView.layer.superlayer == nil { + self.layer.insertSublayer(loadingBackgroundView.layer, above: self.backgroundLayer) + } + loadingBackgroundView.frame = CGRect(origin: .zero, size: size) + } + } else if let loadingBackground = self.loadingBackground { + loadingBackground.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + loadingBackground.view?.layer.removeFromSuperlayer() + }) + self.loadingBackground = nil + } + + let emoji: ChatTextInputTextCustomEmojiAttribute? + var file: TelegramMediaFile? + var animationOffset: CGFloat = 0.0 + switch component.subject { + case let .premium(months): + emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: 0, + file: nil, + custom: .animation(name: "Gift\(months)") + ) + case let .starGift(_, fileValue): + file = fileValue + emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: fileValue.fileId.id, + file: fileValue + ) + animationOffset = 16.0 + } + + let iconSize = CGSize(width: 88.0, height: 88.0) + if self.animationLayer == nil, let emoji { + let animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: false, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + } + + let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize) + if let animationLayer = self.animationLayer { + transition.setFrame(layer: animationLayer, frame: animationFrame) + } + + if let title = component.title { + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 94.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + } + + if let subtitle = component.subtitle { + let subtitleSize = self.subtitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: 112.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + } + + let buttonColor: UIColor + var isStars = false + if component.price.containsEmoji { + buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) + isStars = !component.isSoldOut + } else { + buttonColor = component.theme.list.itemAccentColor + } + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent( + ButtonContentComponent( + context: component.context, + text: component.price, + color: buttonColor, + isStars: isStars + ) + ), + environment: {}, + containerSize: availableSize + ) + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 10.0), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + + if let ribbon = component.ribbon { + let ribbonTextSize = self.ribbonText.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(11.0), textColor: .white)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + if let ribbonTextView = self.ribbonText.view { + if ribbonTextView.superview == nil { + self.addSubview(self.ribbon) + self.addSubview(ribbonTextView) + } + ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize) + + if self.ribbon.image == nil || themeUpdated { + self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors(theme: component.theme), direction: .diagonal) + } + if let ribbonImage = self.ribbon.image { + self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size) + } + ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0) + ribbonTextView.center = CGPoint(x: size.width - 20.0, y: 20.0) + } + } else { + if self.ribbonText.view?.superview != nil { + self.ribbon.removeFromSuperview() + self.ribbonText.view?.removeFromSuperview() + } + } + + if let peer = component.peer { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + self.addSubview(avatarNode.view) + self.avatarNode = avatarNode + } + + switch peer { + case let .peer(peer): + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) + case .anonymous: + avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true)) + } + + avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0)) + } + + self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + + if component.isHidden { + let hiddenIconBackground: UIVisualEffectView + let hiddenIcon: UIImageView + if let currentBackground = self.hiddenIconBackground, let currentIcon = self.hiddenIcon { + hiddenIconBackground = currentBackground + hiddenIcon = currentIcon + } else { + let blurEffect: UIBlurEffect + if #available(iOS 13.0, *) { + blurEffect = UIBlurEffect(style: .systemThinMaterialDark) + } else { + blurEffect = UIBlurEffect(style: .dark) + } + hiddenIconBackground = UIVisualEffectView(effect: blurEffect) + hiddenIconBackground.clipsToBounds = true + hiddenIconBackground.layer.cornerRadius = 15.0 + self.hiddenIconBackground = hiddenIconBackground + + hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white)) + self.hiddenIcon = hiddenIcon + + self.addSubview(hiddenIconBackground) + hiddenIconBackground.contentView.addSubview(hiddenIcon) + + if !isFirstTime { + hiddenIconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + hiddenIconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let iconSize = CGSize(width: 30.0, height: 30.0) + hiddenIconBackground.frame = iconSize.centered(around: animationFrame.center) + hiddenIcon.frame = CGRect(origin: .zero, size: iconSize) + } else { + if let hiddenIconBackground = self.hiddenIconBackground { + self.hiddenIconBackground = nil + self.hiddenIcon = nil + + hiddenIconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in + hiddenIconBackground.removeFromSuperview() + }) + hiddenIconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + let isStars: Bool + + public init( + context: AccountContext, + text: String, + color: UIColor, + isStars: Bool = false + ) { + self.context = context + self.text = text + self.color = color + self.isStars = isStars + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.isStars != rhs.isStars { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + private var starsLayer: StarsButtonEffectLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color) + let range = (attributedText.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range) + attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 9.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0) + + if component.isStars { + let starsLayer: StarsButtonEffectLayer + if let current = self.starsLayer { + starsLayer = current + } else { + starsLayer = StarsButtonEffectLayer() + self.layer.addSublayer(starsLayer) + self.starsLayer = starsLayer + } + starsLayer.frame = CGRect(origin: .zero, size: size) + starsLayer.update(size: size) + } else { + self.starsLayer?.removeFromSuperlayer() + self.starsLayer = nil + } + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor: UIColor + if component.color.rgb == 0xd3720a { + backgroundColor = UIColor(rgb: 0xffc83d, alpha: 0.2) + } else { + backgroundColor = component.color.withAlphaComponent(0.1) + } + + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class StarsButtonEffectLayer: SimpleLayer { + let emitterLayer = CAEmitterLayer() + + override init() { + super.init() + + self.addSublayer(self.emitterLayer) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + let color = UIColor(rgb: 0xffbe27) + + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 14.0 + emitter.lifetime = 2.0 + emitter.velocity = 12.0 + emitter.velocityRange = 3 + emitter.scale = 0.1 + emitter.scaleRange = 0.08 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + + self.emitterLayer.emitterCells = [emitter] + } + + func update(size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup() + } + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7) + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD new file mode 100644 index 00000000000..8af48899c72 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD @@ -0,0 +1,50 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftOptionsScreen", + module_name = "GiftOptionsScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/ConfettiEffect", + "//submodules/InAppPurchaseManager", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", + "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift new file mode 100644 index 00000000000..f27b14029ee --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -0,0 +1,1109 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import Markdown +import TelegramStringFormatting +import PlainButtonComponent +import BlurredBackgroundComponent +import PremiumStarComponent +import ConfettiEffect +import TextFormat +import GiftItemComponent +import InAppPurchaseManager +import TabSelectorComponent +import GiftSetupScreen +import GiftViewScreen +import UndoUI + +final class GiftOptionsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let starsContext: StarsContext + let peerId: EnginePeer.Id + let premiumOptions: [CachedPremiumGiftOption] + let completion: (() -> Void)? + + init( + context: AccountContext, + starsContext: StarsContext, + peerId: EnginePeer.Id, + premiumOptions: [CachedPremiumGiftOption], + completion: (() -> Void)? + ) { + self.context = context + self.starsContext = starsContext + self.peerId = peerId + self.premiumOptions = premiumOptions + self.completion = completion + } + + static func ==(lhs: GiftOptionsScreenComponent, rhs: GiftOptionsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.premiumOptions != rhs.premiumOptions { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + public enum StarsFilter: Equatable { + case all + case limited + case stars(Int64) + + init(rawValue: Int64) { + switch rawValue { + case 0: + self = .all + case -1: + self = .limited + default: + self = .stars(rawValue) + } + } + + public var rawValue: Int64 { + switch self { + case .all: + return 0 + case .limited: + return -1 + case let .stars(stars): + return stars + } + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let topPanel = ComponentView() + private let topSeparator = ComponentView() + private let cancelButton = ComponentView() + + private let header = ComponentView() + + private let balanceTitle = ComponentView() + private let balanceValue = ComponentView() + private let balanceIcon = ComponentView() + + private let premiumTitle = ComponentView() + private let premiumDescription = ComponentView() + private var premiumItems: [AnyHashable: ComponentView] = [:] + private var inProgressPremiumGift: String? + private let purchaseDisposable = MetaDisposable() + + private let starsTitle = ComponentView() + private let starsDescription = ComponentView() + private var starsItems: [AnyHashable: ComponentView] = [:] + private let tabSelector = ComponentView() + private var starsFilter: StarsFilter = .all + + private var _effectiveStarGifts: ([StarGift], StarsFilter)? + private var effectiveStarGifts: [StarGift]? { + get { + if case .all = self.starsFilter { + return self.state?.starGifts + } else { + if let (currentGifts, currentFilter) = self._effectiveStarGifts, currentFilter == self.starsFilter { + return currentGifts + } else if let allGifts = self.state?.starGifts { + let filteredGifts: [StarGift] = allGifts.filter { + switch self.starsFilter { + case .all: + return true + case .limited: + if $0.availability != nil { + return true + } + case let .stars(stars): + if $0.price == stars { + return true + } + } + return false + } + self._effectiveStarGifts = (filteredGifts, self.starsFilter) + return filteredGifts + } else { + return nil + } + } + } + } + + private var isUpdating: Bool = false + + private var starsStateDisposable: Disposable? + private var starsState: StarsContext.State? + + private var component: GiftOptionsScreenComponent? + private(set) weak var state: State? + private var environment: EnvironmentType? + + private var starsItemsOrigin: CGFloat = 0.0 + + private var chevronImage: (UIImage, PresentationTheme)? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.starsStateDisposable?.dispose() + self.purchaseDisposable.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + private func dismissAllTooltips(controller: ViewController) { + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment, let component = self.component else { + return + } + + let availableWidth = self.scrollView.bounds.width + let contentOffset = self.scrollView.contentOffset.y + + let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0 + if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view { + transition.setAlpha(view: topPanelView, alpha: topPanelAlpha) + transition.setAlpha(view: topSeparator, alpha: topPanelAlpha) + } + + let topInset: CGFloat = environment.navigationHeight - 56.0 + + let premiumTitleInitialPosition = (topInset + 160.0) + let premiumTitleOffsetDelta = premiumTitleInitialPosition - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) + let premiumTitleOffset = contentOffset + max(0.0, min(1.0, contentOffset / premiumTitleOffsetDelta)) * 10.0 + let premiumTitleFraction = max(0.0, min(1.0, premiumTitleOffset / premiumTitleOffsetDelta)) + let premiumTitleScale = 1.0 - premiumTitleFraction * 0.36 + var premiumTitleAdditionalOffset: CGFloat = 0.0 + + let starsTitleOffsetDelta = (topInset + 100.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) + + let starsTitleOffset: CGFloat + let starsTitleFraction: CGFloat + if contentOffset > 350 { + starsTitleOffset = contentOffset + max(0.0, min(1.0, (contentOffset - 350.0) / starsTitleOffsetDelta)) * 10.0 + starsTitleFraction = max(0.0, min(1.0, (starsTitleOffset - 350.0) / starsTitleOffsetDelta)) + if contentOffset > 380.0 { + premiumTitleAdditionalOffset = contentOffset - 380.0 + } + } else { + starsTitleOffset = contentOffset + starsTitleFraction = 0.0 + } + let starsTitleScale = 1.0 - starsTitleFraction * 0.36 + if let starsTitleView = self.starsTitle.view { + transition.setPosition(view: starsTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(topInset + 455.0 - starsTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0))) + transition.setScale(view: starsTitleView, scale: starsTitleScale) + } + + if let premiumTitleView = self.premiumTitle.view { + transition.setPosition(view: premiumTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(premiumTitleInitialPosition - premiumTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) - premiumTitleAdditionalOffset)) + transition.setScale(view: premiumTitleView, scale: premiumTitleScale) + } + + if let headerView = self.header.view { + transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale)) + transition.setScale(view: headerView, scale: premiumTitleScale) + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + if let starGifts = self.effectiveStarGifts { + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: self.starsItemsOrigin), size: starsOptionSize) + + let controller = environment.controller + + for gift in starGifts { + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } + + if isVisible { + let itemId = AnyHashable(gift.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.starsItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.starsItems[itemId] = visibleItem + } + + var ribbon: GiftItemComponent.Ribbon? + if let _ = gift.soldOut { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_SoldOut, + color: .red + ) + } else if let _ = gift.availability { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_Limited, + color: .blue + ) + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + peer: nil, + subject: .starGift(gift.id, gift.file), + price: "⭐️ \(gift.price)", + ribbon: ribbon, + isSoldOut: gift.soldOut != nil + ) + ), + effectAlignment: .center, + action: { [weak self] in + if let self, let component = self.component { + if let controller = controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + if gift.availability?.remains == 0 { + let giftController = GiftViewScreen( + context: component.context, + subject: .soldOutGift(gift) + ) + mainController.push(giftController) + } else { + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .starGift(gift), + completion: component.completion + ) + mainController.push(giftController) + } + + } + } + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: starsOptionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollView.addSubview(itemView) + if !transition.animation.isImmediate { + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) + transition.animateScale(view: itemView, from: 0.01, to: 1.0) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > availableWidth { + itemFrame.origin.x = sideInset + itemFrame.origin.y += starsOptionSize.height + optionSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.starsItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.starsItems.removeValue(forKey: id) + } + } + } + + func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let controller = environment.controller + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + self.state = state + + if self.component == nil { + self.starsStateDisposable = (component.starsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state + if !self.isUpdating { + self.state?.updated() + } + }) + } + self.component = component + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let theme = environment.theme + let strings = environment.strings + + let textColor = theme.list.itemPrimaryTextColor + let accentColor = theme.list.itemAccentColor + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + let _ = bottomContentInset + let _ = sectionSpacing + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight - 56.0 + 188.0 + + let headerSize = self.header.update( + transition: .immediate, + component: AnyComponent( + GiftAvatarComponent( + context: component.context, + theme: theme, + peers: state.peer.flatMap { [$0] } ?? [], + isVisible: true, + hasIdleAnimations: true, + color: UIColor(rgb: 0xf9b004), + hasLargeParticles: true + ) + ), + environment: {}, + containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0) + ) + if let headerView = self.header.view { + if headerView.superview == nil { + self.addSubview(headerView) + } + transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize)) + } + + let topPanelSize = self.topPanel.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: theme.rootController.navigationBar.blurredBackgroundColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight) + ) + + let topSeparatorSize = self.topSeparator.update( + transition: transition, + component: AnyComponent(Rectangle( + color: theme.rootController.navigationBar.separatorColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: UIScreenPixel) + ) + let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height)) + let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height)) + if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view { + if topPanelView.superview == nil { + self.addSubview(topPanelView) + self.addSubview(topSeparatorView) + } + transition.setFrame(view: topPanelView, frame: topPanelFrame) + transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame) + } + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)), + horizontalAlignment: .center + ) + ), + effectAlignment: .center, + action: { + controller()?.dismiss() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + + let balanceTitleSize = self.balanceTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_Purchase_Balance, + font: Font.regular(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + let balanceValueSize = self.balanceValue.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator), + font: Font.semibold(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + let balanceIconSize = self.balanceIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), + environment: {}, + containerSize: availableSize + ) + + if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view { + if balanceTitleView.superview == nil { + self.addSubview(balanceTitleView) + self.addSubview(balanceValueView) + self.addSubview(balanceIconView) + } + let navigationHeight = environment.navigationHeight - environment.statusBarHeight + let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0 + balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0) + balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize) + balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0) + balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) + balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) + balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) + } + + let premiumTitleSize = self.premiumTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Gift_Options_Premium_Title, font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + if let premiumTitleView = self.premiumTitle.view { + if premiumTitleView.superview == nil { + self.addSubview(premiumTitleView) + } + transition.setBounds(view: premiumTitleView, bounds: CGRect(origin: .zero, size: premiumTitleSize)) + } + + if self.chevronImage == nil || self.chevronImage?.1 !== theme { + self.chevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme) + } + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let peerName = state.peer?.compactDisplayTitle ?? "" + + let premiumDescriptionString = parseMarkdownIntoAttributedString(strings.Gift_Options_Premium_Text(peerName).string, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = premiumDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 { + premiumDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: premiumDescriptionString.string)) + } + let premiumDescriptionSize = self.premiumDescription.update( + transition: transition, + component: AnyComponent(BalancedTextComponent( + text: .plain(premiumDescriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let introController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil) + introController.navigationPresentation = .modal + + if let controller = environment.controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + mainController.push(introController) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let premiumDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumDescriptionSize.width) / 2.0), y: contentHeight), size: premiumDescriptionSize) + if let premiumDescriptionView = self.premiumDescription.view { + if premiumDescriptionView.superview == nil { + self.scrollView.addSubview(premiumDescriptionView) + } + transition.setFrame(view: premiumDescriptionView, frame: premiumDescriptionFrame) + } + contentHeight += premiumDescriptionSize.height + contentHeight += 11.0 + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + + if let premiumProducts = state.premiumProducts { + let premiumOptionSize = CGSize(width: optionWidth, height: 178.0) + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize) + for product in premiumProducts { + let itemId = AnyHashable(product.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.premiumItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.premiumItems[itemId] = visibleItem + } + + let title: String + switch product.months { + case 6: + title = strings.Gift_Options_Premium_Months(6) + case 12: + title = strings.Gift_Options_Premium_Years(1) + default: + title = strings.Gift_Options_Premium_Months(3) + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + peer: nil, + subject: .premium(product.months), + title: title, + subtitle: strings.Gift_Options_Premium_Premium, + price: product.price, + ribbon: product.discount.flatMap { + GiftItemComponent.Ribbon( + text: "-\($0)%", + color: .red + ) + }, + isLoading: self.inProgressPremiumGift == product.id + ) + ), + effectAlignment: .center, + action: { [weak self] in + if let self, let component = self.component { + if let controller = controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .premium(product), + completion: component.completion + ) + mainController.push(giftController) + } + } + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: premiumOptionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollView.addSubview(itemView) + if !transition.animation.isImmediate { + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > availableSize.width { + itemFrame.origin.x = sideInset + itemFrame.origin.y += premiumOptionSize.height + optionSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.premiumItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.premiumItems.removeValue(forKey: id) + } + + contentHeight += ceil(CGFloat(premiumProducts.count) / 3.0) * premiumOptionSize.height + contentHeight += 66.0 + } + + + let starsTitleSize = self.starsTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Gift_Options_Gift_Title, font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + if let starsTitleView = self.starsTitle.view { + if starsTitleView.superview == nil { + self.addSubview(starsTitleView) + } + transition.setBounds(view: starsTitleView, bounds: CGRect(origin: .zero, size: starsTitleSize)) + } + + let starsDescriptionString = parseMarkdownIntoAttributedString(strings.Gift_Options_Gift_Text(peerName).string, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = starsDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 { + starsDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsDescriptionString.string)) + } + let starsDescriptionSize = self.starsDescription.update( + transition: transition, + component: AnyComponent(BalancedTextComponent( + text: .plain(starsDescriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) + if let controller = environment.controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + mainController.push(introController) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let starsDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - starsDescriptionSize.width) / 2.0), y: contentHeight), size: starsDescriptionSize) + if let starsDescriptionView = self.starsDescription.view { + if starsDescriptionView.superview == nil { + self.scrollView.addSubview(starsDescriptionView) + } + transition.setFrame(view: starsDescriptionView, frame: starsDescriptionFrame) + } + contentHeight += starsDescriptionSize.height + contentHeight += 16.0 + + var tabSelectorItems: [TabSelectorComponent.Item] = [] + tabSelectorItems.append(TabSelectorComponent.Item( + id: AnyHashable(StarsFilter.all.rawValue), + title: strings.Gift_Options_Gift_Filter_AllGifts + )) + + var hasLimited = false + var starsAmountsSet = Set() + if let starGifts = self.state?.starGifts { + for product in starGifts { + starsAmountsSet.insert(product.price) + if product.availability != nil { + hasLimited = true + } + } + } + + if hasLimited { + tabSelectorItems.append(TabSelectorComponent.Item( + id: AnyHashable(StarsFilter.limited.rawValue), + title: strings.Gift_Options_Gift_Filter_Limited + )) + } + + let starsAmounts = Array(starsAmountsSet).sorted() + for amount in starsAmounts { + tabSelectorItems.append(TabSelectorComponent.Item( + id: AnyHashable(StarsFilter.stars(amount).rawValue), + title: "⭐️\(amount)" + )) + } + + let tabSelectorSize = self.tabSelector.update( + transition: transition, + component: AnyComponent(TabSelectorComponent( + context: component.context, + colors: TabSelectorComponent.Colors( + foreground: theme.list.itemSecondaryTextColor, + selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), + simple: true + ), + items: tabSelectorItems, + selectedId: AnyHashable(self.starsFilter.rawValue), + setSelectedId: { [weak self] id in + guard let self, let idValue = id.base as? Int64 else { + return + } + let starsFilter = StarsFilter(rawValue: idValue) + if self.starsFilter != starsFilter { + self.starsFilter = starsFilter + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) + ) + if let tabSelectorView = self.tabSelector.view { + if tabSelectorView.superview == nil { + self.scrollView.addSubview(tabSelectorView) + } + transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize)) + } + contentHeight += tabSelectorSize.height + contentHeight += 19.0 + + if let starGifts = self.effectiveStarGifts { + self.starsItemsOrigin = contentHeight + + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * starsOptionSize.height + contentHeight += 66.0 + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + private var updateDisposable: Disposable? + + fileprivate var peer: EnginePeer? + fileprivate var premiumProducts: [PremiumGiftProduct]? + fileprivate var starGifts: [StarGift]? + + init( + context: AccountContext, + peerId: EnginePeer.Id, + premiumOptions: [CachedPremiumGiftOption] + ) { + self.context = context + + super.init() + + let availableProducts: Signal<[InAppPurchaseManager.Product], NoError> + if let inAppPurchaseManager = context.inAppPurchaseManager { + availableProducts = inAppPurchaseManager.availableProducts + } else { + availableProducts = .single([]) + } + + self.disposable = combineLatest( + queue: Queue.mainQueue(), + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer.init(id: peerId) + ), + availableProducts, + context.engine.payments.cachedStarGifts() + ).start(next: { [weak self] peer, availableProducts, starGifts in + guard let self, let peer else { + return + } + self.peer = peer + + if availableProducts.isEmpty { + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + premiumProducts.append( + PremiumGiftProduct( + giftOption: CachedPremiumGiftOption( + months: option.months, + currency: option.currency, + amount: option.amount, + botUrl: "", + storeProductId: option.storeProductId + ), + storeProduct: nil, + discount: nil + ) + ) + } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) + } else { + let shortestOptionPrice: (Int64, NSDecimalNumber) + if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) { + shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) + } else { + shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) + } + + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { + let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) + let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) + premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + } + } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) + } + + self.starGifts = starGifts + + self.updated() + }) + + self.updateDisposable = self.context.engine.payments.keepStarGiftsUpdated().start() + } + + deinit { + self.disposable?.dispose() + self.updateDisposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, peerId: self.peerId, premiumOptions: self.premiumOptions) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { + private let context: AccountContext + + public var parentController: () -> ViewController? = { + return nil + } + + public init( + context: AccountContext, + starsContext: StarsContext, + peerId: EnginePeer.Id, + premiumOptions: [CachedPremiumGiftOption], + completion: (() -> Void)? = nil + ) { + self.context = context + + super.init(context: context, component: GiftOptionsScreenComponent( + context: context, + starsContext: starsContext, + peerId: peerId, + premiumOptions: premiumOptions, + completion: completion + ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) + + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? GiftOptionsScreenComponent.View else { + return + } + componentView.scrollToTop() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD new file mode 100644 index 00000000000..46415335fb8 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -0,0 +1,51 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftSetupScreen", + module_name = "GiftSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramStringFormatting", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/AppBundle", + "//submodules/WallpaperBackgroundNode", + "//submodules/TextFormat", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/BotPaymentsUI", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/InAppPurchaseManager", + "//submodules/Components/BlurredBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift new file mode 100644 index 00000000000..4ec4cfbe931 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -0,0 +1,395 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode +import ListItemComponentAdaptor + +final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + enum Subject: Equatable { + case premium(months: Int32, amount: Int64, currency: String) + case starGift(gift: StarGift) + } + let context: AccountContext + let theme: PresentationTheme + let componentTheme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + + let accountPeer: EnginePeer? + let subject: ChatGiftPreviewItem.Subject + let text: String + let entities: [MessageTextEntity] + + init( + context: AccountContext, + theme: PresentationTheme, + componentTheme: PresentationTheme, + strings: PresentationStrings, + sectionId: ItemListSectionId, + fontSize: PresentationFontSize, + chatBubbleCorners: PresentationChatBubbleCorners, + wallpaper: TelegramWallpaper, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + accountPeer: EnginePeer?, + subject: ChatGiftPreviewItem.Subject, + text: String, + entities: [MessageTextEntity] + ) { + self.context = context + self.theme = theme + self.componentTheme = componentTheme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.accountPeer = accountPeer + self.subject = subject + self.text = text + self.entities = entities + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ChatGiftPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ChatGiftPreviewItemNode { + 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() + }) + } + } + } + } + } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ChatGiftPreviewItem, rhs: ChatGiftPreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.chatBubbleCorners != rhs.chatBubbleCorners { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.accountPeer != rhs.accountPeer { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.entities != rhs.entities { + return false + } + return true + } +} + +final class ChatGiftPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:] + + private var item: ChatGiftPreviewItem? + + private let disposable = MetaDisposable() + + private var initialBubbleHeight: CGFloat? + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.containerNode) + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: ChatGiftPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) + } + + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)) + + var items: [ListViewItem] = [] + for _ in 0 ..< 1 { + let authorPeerId = item.context.account.peerId + + var peers = SimpleDictionary() + let messages = SimpleDictionary() + + peers[authorPeerId] = item.accountPeer?._asPeer() + + let media: [Media] + switch item.subject { + case let .premium(months, amount, currency): + media = [ + TelegramMediaAction( + action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities) + ) + ] + case let .starGift(gift): + media = [ + TelegramMediaAction( + action: .starGift(gift: gift, convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false) + ) + ] + } + + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) + } + + var nodes: [ListViewItemNode] = [] + if let messageNodes = currentNodes { + nodes = messageNodes + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + itemNode.visibility = .visible(1.0, .infinite) + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + itemNode!.visibility = .visible(1.0, .infinite) + messageNodes.append(itemNode!) + + self.initialBubbleHeight = itemNode?.frame.height + } + nodes = messageNodes + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + contentSize.height = 346.0 + insets = itemListNeighborsGroupedInsets(neighbors, params) + if params.width <= 320.0 { + insets.top = 0.0 + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: .zero) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + strongSelf.messageNodes = nodes + //var topOffset: CGFloat = 4.0 + for node in nodes { + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + let bubbleHeight: CGFloat + if let initialBubbleHeight = strongSelf.initialBubbleHeight { + bubbleHeight = max(node.frame.height, initialBubbleHeight) + } else { + bubbleHeight = node.frame.height + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - bubbleHeight) / 2.0)), size: node.frame.size), within: layoutSize) + //topOffset += node.frame.size.height + } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + + var hasTopCorners = false + var hasBottomCorners = false + + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + + let displayMode: WallpaperDisplayMode + if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 { + displayMode = .halfAspectFill + } else { + if backgroundFrame.width > backgroundFrame.height * 4.0 { + if params.availableHeight < 700.0 { + displayMode = .halfAspectFill + } else { + displayMode = .aspectFill + } + } else { + displayMode = .aspectFill + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame + backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate) + } + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift new file mode 100644 index 00000000000..465ffb2c88e --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -0,0 +1,1413 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import ListSectionComponent +import ListActionItemComponent +import ListMultilineTextFieldItemComponent +import ListItemComponentAdaptor +import BundleIconComponent +import LottieComponent +import TextFieldComponent +import ButtonComponent +import BotPaymentsUI +import ChatEntityKeyboardInputNode +import EmojiSuggestionsComponent +import ChatPresentationInterfaceState +import AudioToolbox +import TextFormat +import InAppPurchaseManager +import BlurredBackgroundComponent + +final class GiftSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: EnginePeer.Id + let subject: GiftSetupScreen.Subject + let completion: (() -> Void)? + + init( + context: AccountContext, + peerId: EnginePeer.Id, + subject: GiftSetupScreen.Subject, + completion: (() -> Void)? = nil + ) { + self.context = context + self.peerId = peerId + self.subject = subject + self.completion = completion + } + + static func ==(lhs: GiftSetupScreenComponent, rhs: GiftSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let remainingCount = ComponentView() + private let introContent = ComponentView() + private let introSection = ComponentView() + private let hideSection = ComponentView() + + private let buttonBackground = ComponentView() + private let buttonSeparator = SimpleLayer() + private let button = ComponentView() + + private var ignoreScrolling: Bool = false + private var isUpdating: Bool = false + + private var component: GiftSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private let introPlaceholderTag = NSObject() + private let textInputState = ListMultilineTextFieldItemComponent.ExternalState() + private let textInputTag = NSObject() + private var resetText: String? + + private var currentInputMode: ListMultilineTextFieldItemComponent.InputMode = .keyboard + + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + private var inputMediaNodeBackground = SimpleLayer() + private var inputMediaNodeTargetTag: AnyObject? + private let inputMediaNodeDataPromise = Promise() + + private var currentEmojiSuggestionView: ComponentHostView? + + private var hideName = false + private var inProgress = false + + private var previousHadInputHeight: Bool = false + private var previousInputHeight: CGFloat? + private var recenterOnTag: NSObject? + + private var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + private var starImage: (UIImage, PresentationTheme)? + + private var optionsDisposable: Disposable? + private(set) var options: [StarsTopUpOption] = [] { + didSet { + self.optionsPromise.set(self.options) + } + } + private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + + self.disablesInteractiveKeyboardGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private var scrolledUp = true + private func updateScrolling(transition: ComponentTransition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + + let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + self.buttonBackground.view?.alpha = bottomPanelAlpha + self.buttonSeparator.opacity = Float(bottomPanelAlpha) + } + + func proceed() { + guard let component = self.component else { + return + } + switch component.subject { + case .premium: + self.proceedWithPremiumGift() + case .starGift: + self.proceedWithStarGift() + } + } + + func proceedWithPremiumGift() { + guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else { + return + } + + self.inProgress = true + self.state?.updated() + + let (currency, amount) = storeProduct.priceCurrencyAndAmount + + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") + + let entities = generateChatInputTextEntities(self.textInputState.text) + let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities) + let quantity: Int32 = 1 + + let completion = component.completion + + let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + guard let self else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + if available { + let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let completion { + completion() + } else { + guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + } + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") + + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } + }) + } else { + self.inProgress = false + self.state?.updated(transition: .immediate) + } + }) + } + + func proceedWithStarGift() { + guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + return + } + + let proceed = { [weak self] in + guard let self else { + return + } + + self.inProgress = true + self.state?.updated() + + let entities = generateChatInputTextEntities(self.textInputState.text) + let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) + + let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let completion = component.completion + + let _ = (inputData + |> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in + guard let inputData else { + return + } + let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let completion { + completion() + + if let self, let controller = self.environment?.controller() { + controller.dismiss() + } + } else { + guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + } + + starsContext.load(force: true) + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + + self.inProgress = false + self.state?.updated() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var errorText: String? + switch error { + case .starGiftOutOfStock: + errorText = presentationData.strings.Gift_Send_ErrorOutOfStock + default: + errorText = presentationData.strings.Gift_Send_ErrorUnknown + } + + if let errorText = errorText { + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } + }) + }) + } + + if starsState.balance < starGift.price { + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen( + context: component.context, + starsContext: starsContext, + options: options ?? [], + purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price), + completion: { [weak starsContext] stars in + starsContext?.add(balance: stars) + Queue.mainQueue().after(0.1) { + proceed() + } + } + ) + controller.push(purchaseController) + }) + } else { + proceed() + } + } + + @objc private func previewTap() { + func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false + } + + self.currentInputMode = .keyboard + if hasFirstResponder(self) { + if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + if titleView.isActive { + titleView.deactivateInput() + } else { + self.endEditing(true) + } + } + } else { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + + func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in + guard let self else { + return + } + if let peer { + self.peerMap[peer.id] = peer + } + if let accountPeer { + self.peerMap[accountPeer.id] = accountPeer + } + + self.state?.updated() + }) + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: component.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + forceHasPremium: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + let _ = self + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + textInputView.insertText(text: text) + } + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + if self.textInputState.isEditing { + textInputView.backwardsDeleteText() + } + } + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + guard let controller = self.environment?.controller() as? GiftSetupScreen else { + return nil + } + + if let navigationController = controller.navigationController as? NavigationController { + return navigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: ComponentTransition(transition)) + } + } + ) + + if case .starGift = component.subject { + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: ComponentTransition + if !transition.animation.isImmediate { + alphaTransition = .easeInOut(duration: 0.25) + } else { + alphaTransition = .immediate + } + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let _ = alphaTransition + let _ = presentationData + + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Gift_Send_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + contentHeight += 26.0 + + if case let .starGift(starGift) = component.subject, let availability = starGift.availability { + let remains: Int32 = availability.remains + let total: Int32 = availability.total + let position = CGFloat(remains) / CGFloat(total) + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator) + let remainingCountSize = self.remainingCount.update( + transition: transition, + component: AnyComponent(RemainingCountComponent( + inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), + activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], + inactiveTitle: environment.strings.Gift_Send_Limited, + inactiveValue: "", + inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, + activeTitle: "", + activeValue: totalString, + activeTitleColor: .white, + badgeText: "\(remainsString)", + badgePosition: position, + badgeGraphPosition: position, + invertProgress: true, + leftString: environment.strings.Gift_Send_Remains(remains).replacingOccurrences(of: remainsString, with: "").trimmingCharacters(in: .whitespacesAndNewlines), + groupingSeparator: environment.dateTimeFormat.groupingSeparator + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight - 36.0), size: remainingCountSize) + if let remainingCountView = self.remainingCount.view { + if remainingCountView.superview == nil { + self.scrollView.addSubview(remainingCountView) + } + transition.setFrame(view: remainingCountView, frame: remainingCountFrame) + } + contentHeight += remainingCountSize.height + contentHeight -= 36.0 + contentHeight += sectionSpacing + } + + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + + var introSectionItems: [AnyComponentWithIdentity] = [] + introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag)))) + introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: self.textInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + initialText: "", + resetText: self.resetText.flatMap { + return ListMultilineTextFieldItemComponent.ResetText(value: $0) + }, + placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder, + autocapitalizationType: .sentences, + autocorrectionType: .yes, + returnKeyType: .done, + characterLimit: Int(giftConfiguration.maxCaptionLength), + displayCharacterLimit: true, + emptyLineHandling: .notAllowed, + formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), + updated: { _ in + }, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + titleView.endEditing(true) + } + }, + textUpdateTransition: .spring(duration: 0.4), + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.textInputTag + )))) + self.resetText = nil + + let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" + let introFooter: AnyComponent? + switch component.subject { + case .premium: + introFooter = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_Customize_Info(peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )) + case .starGift: + introFooter = nil + } + + let introSectionSize = self.introSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: introFooter, + items: introSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize) + if let introSectionView = self.introSection.view { + if introSectionView.superview == nil { + self.scrollView.addSubview(introSectionView) + self.introSection.parentState = state + } + transition.setFrame(view: introSectionView, frame: introSectionFrame) + } + contentHeight += introSectionSize.height + contentHeight += sectionSpacing + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight { + inputHeight = previousInputHeight + } else { + inputHeight = environment.inputHeight + } + } + + let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + if let accountPeer = self.peerMap[component.context.account.peerId] { + let subject: ChatGiftPreviewItem.Subject + switch component.subject { + case let .premium(product): + let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) + subject = .premium(months: product.months, amount: amount, currency: currency) + case let .starGift(gift): + subject = .starGift(gift: gift) + } + + let introContentSize = self.introContent.update( + transition: transition, + component: AnyComponent( + ListItemComponentAdaptor( + itemGenerator: ChatGiftPreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: presentationData.chatWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + accountPeer: accountPeer, + subject: subject, + text: self.textInputState.text.string, + entities: generateChatInputTextEntities(self.textInputState.text) + ), + params: listItemParams + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + if let introContentView = self.introContent.view { + if introContentView.superview == nil { + if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { + placeholderView.addSubview(introContentView) + + placeholderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap))) + } + } + transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) + } + } + + if case .starGift = component.subject { + let hideSectionSize = self.hideSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in + guard let self else { + return + } + self.hideName = !self.hideName + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) + if let hideSectionView = self.hideSection.view { + if hideSectionView.superview == nil { + self.scrollView.addSubview(hideSectionView) + } + transition.setFrame(view: hideSectionView, frame: hideSectionFrame) + } + contentHeight += hideSectionSize.height + } + + contentHeight += bottomContentInset + + let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) + contentHeight += combinedBottomInset + + if self.starImage == nil || self.starImage?.1 !== environment.theme { + self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) + } + + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let bottomPanelSize = self.buttonBackground.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: environment.theme.rootController.tabBar.backgroundColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) + ) + self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor + + if let view = self.buttonBackground.view { + if view.superview == nil { + self.addSubview(view) + self.layer.addSublayer(self.buttonSeparator) + } + view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize) + self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + } + + var buttonIsEnabled = true + let buttonString: String + switch component.subject { + case let .premium(product): + let amountString = product.price + buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" + case let .starGift(starGift): + let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) + buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + if let availability = starGift.availability, availability.remains == 0 { + buttonIsEnabled = false + } + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: buttonIsEnabled, + displaysProgress: self.inProgress, + action: { [weak self] in + self?.proceed() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) + } + + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { + emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) + |> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in + guard let self, self.textInputState.currentEmojiSuggestion === emojiSuggestion else { + return + } + + emojiSuggestion?.value = result + self.state?.updated() + }) + } + + var hasTrackingView = self.textInputState.hasTrackingView + if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { + hasTrackingView = false + } + if !self.textInputState.isEditing { + hasTrackingView = false + } + + if !hasTrackingView { + if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion { + self.textInputState.currentEmojiSuggestion = nil + currentEmojiSuggestion.disposable?.dispose() + } + + if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + self.currentEmojiSuggestionView = nil + + currentEmojiSuggestionView.alpha = 0.0 + currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in + currentEmojiSuggestionView?.removeFromSuperview() + }) + } + } + + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { + let currentEmojiSuggestionView: ComponentHostView + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + self.currentEmojiSuggestionView = currentEmojiSuggestionView + self.addSubview(currentEmojiSuggestionView) + + currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + let globalPosition: CGPoint + if let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView { + globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) + } else { + globalPosition = .zero + } + + let sideInset: CGFloat = 7.0 + + let viewSize = currentEmojiSuggestionView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: component.context, + userLocation: .other, + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + files: value, + action: { [weak self] file in + guard let self, let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView, let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion else { + return + } + + AudioServicesPlaySystemSound(0x450) + + let inputState = textView.getInputState() + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + let range = currentEmojiSuggestion.position.range + let previousText = inputText.attributedSubstring(from: range) + inputText.replaceCharacters(in: range, with: replacementText) + + var replacedUpperBound = range.lowerBound + while true { + if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { + let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) + if replaceRange.location < 0 { + break + } + let adjacentString = inputText.attributedSubstring(from: replaceRange) + if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { + break + } + inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) + replacedUpperBound = replaceRange.lowerBound + } else { + break + } + } + + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) + currentEmojiSuggestionView.frame = viewFrame + if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) + } + } + + + let previousBounds = self.scrollView.bounds + + self.recenterOnTag = nil + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { + if let textView = self.introSection.findTaggedView(tag: self.textInputTag) { + if targetView.isDescendant(of: textView) { + self.recenterOnTag = self.textInputTag + } + } + } + if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode { + if self.textInputState.isEditing { + self.recenterOnTag = self.textInputTag + } + } + self.previousHadInputHeight = inputHeight > 0.0 + self.previousInputHeight = inputHeight + + self.ignoreScrolling = true + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + if let recenterOnTag = self.recenterOnTag { + self.recenterOnTag = nil + + if let targetView = self.introSection.findTaggedView(tag: recenterOnTag) { + let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) + var scrollViewBounds = self.scrollView.bounds + let minButtonDistance: CGFloat = 16.0 + if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance { + scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY) + if scrollViewBounds.origin.y < 0.0 { + scrollViewBounds.origin.y = 0.0 + } + } + if self.scrollView.bounds != scrollViewBounds { + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + } + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + return availableSize + } + + private func updateInputMediaNode( + component: GiftSetupScreenComponent, + availableSize: CGSize, + bottomInset: CGFloat, + inputHeight: CGFloat, + effectiveInputHeight: CGFloat, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + transition: ComponentTransition + ) -> CGFloat { + let bottomInset: CGFloat = bottomInset + 8.0 + let bottomContainerInset: CGFloat = 0.0 + let needsInputActivation: Bool = !"".isEmpty + + var height: CGFloat = 0.0 + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + var inputMediaNodeTransition = transition + var animateIn = false + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + animateIn = true + inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) + inputMediaNode = ChatEntityKeyboardInputNode( + context: component.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: true, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext, + forceHasPremium: true + ) + inputMediaNode.clipsToBounds = true + + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + if inputMediaNode.view.superview == nil { + self.inputMediaNodeBackground.removeAllAnimations() + self.layer.addSublayer(self.inputMediaNodeBackground) + self.addSubview(inputMediaNode.view) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + + let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) + + if needsInputActivation { + let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + if animateIn { + var targetFrame = inputNodeFrame + targetFrame.origin.y = availableSize.height + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) + + let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) + + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) + + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } else { + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + height = heightAndOverflow.0 + } else { + self.inputMediaNodeTargetTag = nil + + if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + var targetFrame = inputMediaNode.frame + targetFrame.origin.y = availableSize.height + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.3) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + guard let self else { + return + } + if self.currentInputMode == .keyboard { + self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in + guard let self else { + return + } + + if finished { + self.inputMediaNodeBackground.removeFromSuperlayer() + } + self.inputMediaNodeBackground.removeAllAnimations() + }) + } + } + }) + } + } + + return height + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class GiftSetupScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case premium(PremiumGiftProduct) + case starGift(StarGift) + } + + private let context: AccountContext + + public init( + context: AccountContext, + peerId: EnginePeer.Id, + subject: Subject, + completion: (() -> Void)? = nil + ) { + self.context = context + + super.init(context: context, component: GiftSetupScreenComponent( + context: context, + peerId: peerId, + subject: subject, + completion: completion + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + self.title = "" + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} + +private struct GiftConfiguration { + static var defaultValue: GiftConfiguration { + return GiftConfiguration(maxCaptionLength: 255) + } + + let maxCaptionLength: Int32 + + fileprivate init(maxCaptionLength: Int32) { + self.maxCaptionLength = maxCaptionLength + } + + static func with(appConfiguration: AppConfiguration) -> GiftConfiguration { + if let data = appConfiguration.data { + var maxCaptionLength: Int32? + if let value = data["stargifts_message_length_max"] as? Double { + maxCaptionLength = Int32(value) + } + return GiftConfiguration(maxCaptionLength: maxCaptionLength ?? GiftConfiguration.defaultValue.maxCaptionLength) + } else { + return .defaultValue + } + } +} + +public struct PremiumGiftProduct: Equatable { + public let giftOption: CachedPremiumGiftOption + public let storeProduct: InAppPurchaseManager.Product? + public let discount: Int? + + public var id: String { + return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "") + } + + public var months: Int32 { + return self.giftOption.months + } + + public var price: String { + return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency) + } + + public var pricePerMonth: String { + return self.storeProduct?.pricePerMonth(Int(self.months)) ?? "" + } + + public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) { + self.giftOption = giftOption + self.storeProduct = storeProduct + self.discount = discount + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift new file mode 100644 index 00000000000..bd1e466aba0 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -0,0 +1,883 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import MultilineTextComponent +import Markdown +import TextFormat +import RoundedRectWithTailPath + +public class RemainingCountComponent: Component { + private let inactiveColor: UIColor + private let activeColors: [UIColor] + private let inactiveTitle: String + private let inactiveValue: String + private let inactiveTitleColor: UIColor + private let activeTitle: String + private let activeValue: String + private let activeTitleColor: UIColor + private let badgeText: String? + private let badgePosition: CGFloat + private let badgeGraphPosition: CGFloat + private let invertProgress: Bool + private let leftString: String + private let groupingSeparator: String + + public init( + inactiveColor: UIColor, + activeColors: [UIColor], + inactiveTitle: String, + inactiveValue: String, + inactiveTitleColor: UIColor, + activeTitle: String, + activeValue: String, + activeTitleColor: UIColor, + badgeText: String?, + badgePosition: CGFloat, + badgeGraphPosition: CGFloat, + invertProgress: Bool = false, + leftString: String, + groupingSeparator: String + ) { + self.inactiveColor = inactiveColor + self.activeColors = activeColors + self.inactiveTitle = inactiveTitle + self.inactiveValue = inactiveValue + self.inactiveTitleColor = inactiveTitleColor + self.activeTitle = activeTitle + self.activeValue = activeValue + self.activeTitleColor = activeTitleColor + self.badgeText = badgeText + self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition + self.invertProgress = invertProgress + self.leftString = leftString + self.groupingSeparator = groupingSeparator + } + + public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.activeColors != rhs.activeColors { + return false + } + if lhs.inactiveTitle != rhs.inactiveTitle { + return false + } + if lhs.inactiveValue != rhs.inactiveValue { + return false + } + if lhs.inactiveTitleColor != rhs.inactiveTitleColor { + return false + } + if lhs.activeTitle != rhs.activeTitle { + return false + } + if lhs.activeValue != rhs.activeValue { + return false + } + if lhs.activeTitleColor != rhs.activeTitleColor { + return false + } + if lhs.badgeText != rhs.badgeText { + return false + } + if lhs.badgePosition != rhs.badgePosition { + return false + } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } + if lhs.invertProgress != rhs.invertProgress { + return false + } + if lhs.leftString != rhs.leftString { + return false + } + if lhs.groupingSeparator != rhs.groupingSeparator { + return false + } + return true + } + + public final class View: UIView { + private var component: RemainingCountComponent? + + private let container: UIView + private let inactiveBackground: SimpleLayer + + private let inactiveTitleLabel = ComponentView() + private let inactiveValueLabel = ComponentView() + + private let innerLeftTitleLabel = ComponentView() + private let innerRightTitleLabel = ComponentView() + + private let activeContainer: UIView + private let activeBackground: SimpleLayer + + private let activeTitleLabel = ComponentView() + private let activeValueLabel = ComponentView() + + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = CAShapeLayer() + + private let badgeForeground: SimpleLayer + private var badgeLabel: BadgeLabelView? + private let badgeLeftLabel = ComponentView() + private let badgeLabelMaskView = UIImageView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + override init(frame: CGRect) { + self.container = UIView() + self.container.clipsToBounds = true + self.container.layer.cornerRadius = 9.0 + + self.inactiveBackground = SimpleLayer() + + self.activeContainer = UIView() + self.activeContainer.clipsToBounds = true + + self.activeBackground = SimpleLayer() + self.activeBackground.anchorPoint = CGPoint() + + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + super.init(frame: frame) + + self.addSubview(self.container) + self.container.layer.addSublayer(self.inactiveBackground) + self.container.addSubview(self.activeContainer) + self.activeContainer.layer.addSublayer(self.activeBackground) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + //self.badgeView.addSubview(self.badgeLabel) + + self.badgeLabelMaskView.contentMode = .scaleToFill + self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.badgeShapeAnimator?.invalidate() + } + + private var didPlayAppearanceAnimation = false + func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) { + if from == nil { + self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + let rotationAngle: CGFloat + if badgeFullSize.width > 100.0 { + rotationAngle = 0.2 + } else { + rotationAngle = 0.26 + } + + let to: CGFloat = self.badgeView.center.x + + let positionAnimation = CABasicAnimation(keyPath: "position.x") + positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0)) + positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0)) + positionAnimation.duration = 0.5 + positionAnimation.fillMode = .forwards + positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.badgeView.layer.add(positionAnimation, forKey: "appearance1") + + if from != to { + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 as NSNumber + rotateAnimation.toValue = rotationAngle as NSNumber + rotateAnimation.duration = 0.15 + rotateAnimation.fillMode = .forwards + rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + rotateAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") + + Queue.mainQueue().after(0.5, { + let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + bounceAnimation.fromValue = rotationAngle as NSNumber + bounceAnimation.toValue = -0.04 as NSNumber + bounceAnimation.duration = 0.2 + bounceAnimation.fillMode = .forwards + bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + bounceAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(bounceAnimation, forKey: "appearance3") + self.badgeView.layer.removeAnimation(forKey: "appearance2") + + Queue.mainQueue().after(0.2) { + let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + returnAnimation.fromValue = -0.04 as NSNumber + returnAnimation.toValue = 0.0 as NSNumber + returnAnimation.duration = 0.15 + returnAnimation.fillMode = .forwards + returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + self.badgeView.layer.add(returnAnimation, forKey: "appearance4") + self.badgeView.layer.removeAnimation(forKey: "appearance3") + } + }) + } + + if from == nil { + self.badgeView.alpha = 1.0 + self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel { + let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) + var frameTransition = transition + if from == nil { + frameTransition = frameTransition.withAnimation(.none) + } + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: transition) + frameTransition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize)) + } + } + + var previousAvailableSize: CGSize? + func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor + self.activeBackground.backgroundColor = component.activeColors.last?.cgColor + + let size = CGSize(width: availableSize.width, height: 90.0) + + + if self.badgeLabel == nil { + let badgeLabel = BadgeLabelView(groupingSeparator: component.groupingSeparator) + let _ = badgeLabel.update(value: "0", transition: .immediate) + badgeLabel.mask = self.badgeLabelMaskView + self.badgeLabel = badgeLabel + self.badgeView.addSubview(badgeLabel) + } + + self.badgeLabel?.color = component.activeTitleColor + + let lineHeight: CGFloat = 30.0 + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight)) + self.container.frame = containerFrame + + let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition) + let activeWidth: CGFloat = containerFrame.width - activityPosition + + let leftTextColor: UIColor + let rightTextColor: UIColor + if component.invertProgress { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.inactiveTitleColor + } else { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.activeTitleColor + } + + if component.invertProgress { + let innerLeftTitleSize = self.innerLeftTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerLeftTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize) + } + + let innerRightTitleSize = self.innerRightTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerRightTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize) + } + } + + let inactiveTitleSize = self.inactiveTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) + } + + let inactiveValueSize = self.inactiveValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveValue, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) + } + + let activeTitleSize = self.activeTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeTitle, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) + } + + let activeValueSize = self.activeValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + + if component.invertProgress { + self.container.bringSubviewToFront(self.activeContainer) + } + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + } + + var progressTransition: ComponentTransition = .immediate + if !transition.animation.isImmediate { + progressTransition = .easeInOut(duration: 0.5) + } + if "".isEmpty { + if component.invertProgress { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } else { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))) + progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } + if self.activeBackground.animation(forKey: "movement") == nil { + self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) + } + } + + let countWidth: CGFloat + if let badgeText = component.badgeText { + countWidth = getLabelWidth(badgeText) + } else { + countWidth = 51.0 + } + + let badgeSpacing: CGFloat = 4.0 + + let badgeLeftSize = self.badgeLeftLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.leftString, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.badgeLeftLabel.view { + if view.superview == nil { + self.badgeView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 10.0 + countWidth + badgeSpacing, y: 4.0 + UIScreenPixel), size: badgeLeftSize) + } + + let badgeWidth: CGFloat = countWidth + 20.0 + badgeSpacing + badgeLeftSize.width + let badgeSize = CGSize(width: badgeWidth, height: 30.0) + let badgeFullSize = CGSize(width: badgeWidth, height: badgeSize.height + 8.0) + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) + self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + + let currentBadgeX = self.badgeView.center.x + + let badgePosition = component.badgePosition + + if badgePosition > 1.0 - 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 1.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + } else { + self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0) + } + } else if badgePosition < 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0) + } + } else { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.5 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0) + } + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) + } + + self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) + + if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate { + self.didPlayAppearanceAnimation = true + if transition.animation.isImmediate { + if component.badgePosition < 0.1 { + self.badgeView.alpha = 1.0 + if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel { + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: .immediate) + transition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize)) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX) + } + } + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(component.activeColors.count - 1) + for i in 0 ..< component.activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.activeBackground.contentsGravity = .resizeAspectFill + self.activeBackground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return size + } + + private var badgeShapeAnimator: ConstantDisplayLinkAnimator? + private func animateBadgeTailPositionChange() { + if self.badgeShapeAnimator == nil { + self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBadgeTailPositionChange() + }) + self.badgeShapeAnimator?.isPaused = true + } + + if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments { + self.badgeShapeAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath + + if t >= 1.0 { + self.badgeShapeArguments = nil + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } else { + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } + + private func setupGradientAnimations() { + guard let _ = self.component else { + return + } + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let lineOffset = 0.0 + let linePreviousValue = self.activeBackground.position.x + var lineNewValue: CGFloat = lineOffset + if linePreviousValue < 0.0 { + lineNewValue = 0.0 + } else { + lineNewValue = -self.activeContainer.bounds.width * 0.35 + } + lineNewValue -= self.activeContainer.frame.minX + self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0) + + let badgeAnimation = CABasicAnimation(keyPath: "position.x") + badgeAnimation.duration = 4.5 + badgeAnimation.fromValue = badgePreviousValue + badgeAnimation.toValue = badgeNewValue + badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + self.badgeForeground.add(badgeAnimation, forKey: "movement") + + let lineAnimation = CABasicAnimation(keyPath: "position.x") + lineAnimation.duration = 4.5 + lineAnimation.fromValue = linePreviousValue + lineAnimation.toValue = lineNewValue + lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.activeBackground.add(lineAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + + +private let spaceWidth: CGFloat = 3.0 +private let labelWidth: CGFloat = 10.0 +private let labelHeight: CGFloat = 30.0 +private let labelSize = CGSize(width: labelWidth, height: labelHeight) +private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: []) + +final class BadgeLabelView: UIView { + private class StackView: UIView { + var labels: [UILabel] = [] + + var currentValue: Int32? + + var color: UIColor = .white { + didSet { + for view in self.labels { + view.textColor = self.color + } + } + } + + init(groupingSeparator: String) { + super.init(frame: CGRect(origin: .zero, size: labelSize)) + + var height: CGFloat = -labelHeight * 2.0 + for i in -2 ..< 10 { + let label = UILabel() + let itemWidth: CGFloat + if i == -2 { + label.text = groupingSeparator + itemWidth = spaceWidth + } else if i == -1 { + label.text = "9" + itemWidth = labelWidth + } else { + label.text = "\(i)" + itemWidth = labelWidth + } + label.textColor = self.color + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: itemWidth, height: labelHeight) + self.addSubview(label) + self.labels.append(label) + + height += labelHeight + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(value: Int32?, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + let previousValue = self.currentValue + self.currentValue = value + + self.labels[2].alpha = isFirst && !isLast ? 0.0 : 1.0 + + if let value { + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } else { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -2.0 * labelSize.height + ), + size: labelSize + ) + } + } + } + + private let groupingSeparator: String + private var itemViews: [Int: StackView] = [:] + + init(groupingSeparator: String) { + self.groupingSeparator = groupingSeparator + + super.init(frame: .zero) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var color: UIColor = .white { + didSet { + for (_, view) in self.itemViews { + view.color = self.color + } + } + } + + func update(value: String, transition: ComponentTransition) -> CGSize { + let string = value + let stringArray = Array(string.map { String($0) }.reversed()) + + let totalWidth: CGFloat = getLabelWidth(value) + + var rightX: CGFloat = totalWidth + var validIds: [Int] = [] + for i in 0 ..< stringArray.count { + validIds.append(i) + + let itemView: StackView + var itemTransition = transition + if let current = self.itemViews[i] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = StackView(groupingSeparator: self.groupingSeparator) + itemView.color = self.color + self.itemViews[i] = itemView + self.addSubview(itemView) + } + + let digit = Int32(stringArray[i]) + itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + + let itemWidth: CGFloat = digit != nil ? labelWidth : spaceWidth + rightX -= itemWidth + + itemTransition.setFrame( + view: itemView, + frame: CGRect(x: rightX, y: 0.0, width: labelWidth, height: labelHeight) + ) + } + + var removeIds: [Int] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + return CGSize(width: totalWidth, height: labelHeight) + } +} + +private func getLabelWidth(_ string: String) -> CGFloat { + var totalWidth: CGFloat = 0.0 + for c in string { + if CharacterSet.decimalDigits.contains(c.unicodeScalars[c.unicodeScalars.startIndex]) { + totalWidth += labelWidth + } else { + totalWidth += spaceWidth + } + } + return totalWidth +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD new file mode 100644 index 00000000000..4bd6792c0c1 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -0,0 +1,46 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftViewScreen", + module_name = "GiftViewScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", + "//submodules/UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift new file mode 100644 index 00000000000..6ac2b53c308 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -0,0 +1,1679 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import StarsAvatarComponent +import EmojiTextAttachmentView +import UndoUI +import GiftAnimationComponent + +private final class GiftViewSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: GiftViewScreen.Subject + let cancel: (Bool) -> Void + let openPeer: (EnginePeer) -> Void + let updateSavedToProfile: (Bool) -> Void + let convertToStars: () -> Void + let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void + + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + cancel: @escaping (Bool) -> Void, + openPeer: @escaping (EnginePeer) -> Void, + updateSavedToProfile: @escaping (Bool) -> Void, + convertToStars: @escaping () -> Void, + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.cancel = cancel + self.openPeer = openPeer + self.updateSavedToProfile = updateSavedToProfile + self.convertToStars = convertToStars + self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts + } + + static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + var initialized = false + + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + var starGiftsMap: [Int64: StarGift] = [:] + + var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedSmallChevronImage: (UIImage, PresentationTheme)? + + var inProgress = false + + init(context: AccountContext, subject: GiftViewScreen.Subject) { + self.context = context + + super.init() + + if let arguments = subject.arguments { + var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] + if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) { + peerIds.append(fromPeerId) + } + self.disposable = combineLatest(queue: Queue.mainQueue(), + context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )), + .single(nil) |> then(context.engine.payments.cachedStarGifts()) + ).startStrict(next: { [weak self] peers, starGifts in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + + var starGiftsMap: [Int64: StarGift] = [:] + if let starGifts { + for gift in starGifts { + starGiftsMap[gift.id] = gift + } + } + strongSelf.starGiftsMap = starGiftsMap + + strongSelf.initialized = true + + strongSelf.updated(transition: .immediate) + } + }) + } + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(Button.self) + let animation = Child(GiftAnimationComponent.self) + let title = Child(MultilineTextComponent.self) + let description = Child(MultilineTextComponent.self) + let hiddenText = Child(MultilineTextComponent.self) + let table = Child(TableComponent.self) + let additionalText = Child(MultilineTextComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + + let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + + let component = context.component + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = environment.dateTimeFormat + + let state = context.state + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let titleString: String + let animationFile: TelegramMediaFile? + let stars: Int64 + let convertStars: Int64 + let text: String? + let entities: [MessageTextEntity]? + let limitTotal: Int32? + var incoming = false + var savedToProfile = false + var converted = false + var giftId: Int64 = 0 + var date: Int32? + var soldOut = false + var nameHidden = false + if case let .soldOutGift(gift) = component.subject { + animationFile = gift.file + stars = gift.price + text = nil + entities = nil + limitTotal = gift.availability?.total + convertStars = 0 + soldOut = true + titleString = strings.Gift_View_UnavailableTitle + } else if let arguments = component.subject.arguments { + animationFile = arguments.gift.file + stars = arguments.gift.price + text = arguments.text + entities = arguments.entities + limitTotal = arguments.gift.availability?.total + convertStars = arguments.convertStars + incoming = arguments.incoming || arguments.peerId == component.context.account.peerId + savedToProfile = arguments.savedToProfile + converted = arguments.converted + giftId = arguments.gift.id + date = arguments.date + titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title + nameHidden = arguments.nameHidden + } else { + animationFile = nil + stars = 0 + text = nil + entities = nil + limitTotal = nil + convertStars = 0 + titleString = "" + } + + var descriptionText: String + if soldOut { + descriptionText = strings.Gift_View_UnavailableDescription + } else if incoming { + if !converted { + descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string + } else { + descriptionText = strings.Gift_View_ConvertedDescription(strings.Gift_View_ConvertedDescription_Stars(Int32(convertStars))).string + } + } else if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { + if case .message = component.subject { + descriptionText = strings.Gift_View_OtherDescription(peer.compactDisplayTitle, strings.Gift_View_OtherDescription_Stars(Int32(convertStars))).string + } else { + descriptionText = "" + } + } else { + descriptionText = "" + } + if let spaceRegex { + let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText) + let matches = spaceRegex.matches(in: descriptionText, options: [], range: nsRange) + var modifiedString = descriptionText + + for match in matches.reversed() { + let matchRange = Range(match.range, in: descriptionText)! + let matchedSubstring = String(descriptionText[matchRange]) + let replacedSubstring = matchedSubstring.replacingOccurrences(of: " ", with: "\u{00A0}") + modifiedString.replaceSubrange(matchRange, with: replacedSubstring) + } + descriptionText = modifiedString + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleString, + font: Font.bold(25.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) + ) + + var originY: CGFloat = 0.0 + if let animationFile { + let animation = animation.update( + component: GiftAnimationComponent( + context: component.context, + theme: environment.theme, + file: animationFile + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 25.0)) + ) + originY += animation.size.height + } + originY += 80.0 + if soldOut { + originY -= 12.0 + } + + let linkColor = theme.actionSheet.controlAccentColor + if !descriptionText.isEmpty { + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + let textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) + let textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + let description = description.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.openStarsIntro() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + ) + originY += description.size.height + 21.0 + if soldOut { + originY -= 7.0 + } + } else { + originY += 21.0 + } + + if nameHidden { + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + + let hiddenText = hiddenText.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden, font: textFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(hiddenText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) + ) + + originY += hiddenText.size.height + originY += 11.0 + } + + let tableFont = Font.regular(15.0) + let tableBoldFont = Font.semibold(15.0) + let tableItalicFont = Font.italic(15.0) + let tableBoldItalicFont = Font.semiboldItalic(15.0) + let tableMonospaceFont = Font.monospace(15.0) + + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor + var tableItems: [TableComponent.Item] = [] + + if !soldOut { + if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { + let fromComponent: AnyComponent + if incoming { + fromComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Send, + color: theme.list.itemAccentColor + )), + action: { + component.sendGift(peerId) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + fromComponent = AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + } + tableItems.append(.init( + id: "from", + title: strings.Gift_View_From, + component: fromComponent + )) + } else { + tableItems.append(.init( + id: "from_anon", + title: strings.Gift_View_From, + component: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil + ) + ) + )) + } + } + + if case let .soldOutGift(gift) = component.subject, let soldOut = gift.soldOut { + tableItems.append(.init( + id: "firstDate", + title: strings.Gift_View_FirstSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + + tableItems.append(.init( + id: "lastDate", + title: strings.Gift_View_LastSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } else if let date { + tableItems.append(.init( + id: "date", + title: strings.Gift_View_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } + + let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let valueComponent: AnyComponent + if incoming && !converted { + valueComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Sale(strings.Gift_View_Sale_Stars(Int32(convertStars))).string, + color: theme.list.itemAccentColor + )), + action: { + component.convertToStars() + } + )) + ) + ], spacing: 4.0) + ) + } else { + valueComponent = AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + } + + tableItems.append(.init( + id: "value", + title: strings.Gift_View_Value, + component: valueComponent, + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + + if let limitTotal { + var remains: Int32 = 0 + if let gift = state.starGiftsMap[giftId], let availability = gift.availability { + remains = availability.remains + } + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator) + tableItems.append(.init( + id: "availability", + title: strings.Gift_View_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) + ) + )) + } + + if savedToProfile { + tableItems.append(.init( + id: "visibility", + title: strings.Gift_View_Visibility, + component: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Visibility_Visible, font: tableFont, textColor: tableTextColor)))) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Visibility_Hide, + color: theme.list.itemAccentColor + )), + action: { + component.updateSavedToProfile(false) + } + )) + ) + ], spacing: 4.0) + ), + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + } + + if let text { + let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil) + + tableItems.append(.init( + id: "text", + title: nil, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(attributedText), + maximumNumberOfLines: 0, + handleSpoilers: true + ) + ) + )) + } + + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 23.0 + + if incoming && !converted { + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) + } + let descriptionText = savedToProfile ? strings.Gift_View_DisplayedInfo : strings.Gift_View_HiddenInfo + + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + + originY -= 5.0 + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.openMyGifts() + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) + ) + originY += additionalText.size.height + originY += 16.0 + } + + if incoming && !converted && !savedToProfile { + let button = button.update( + component: SolidRoundedButtonComponent( + title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.updateSavedToProfile(!savedToProfile) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + originY += 7.0 + } else { + let button = button.update( + component: SolidRoundedButtonComponent( + title: strings.Common_OK, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + originY += 7.0 + } + + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class GiftViewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: GiftViewScreen.Subject + let openPeer: (EnginePeer) -> Void + let updateSavedToProfile: (Bool) -> Void + let convertToStars: () -> Void + let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void + + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + openPeer: @escaping (EnginePeer) -> Void, + updateSavedToProfile: @escaping (Bool) -> Void, + convertToStars: @escaping () -> Void, + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.openPeer = openPeer + self.updateSavedToProfile = updateSavedToProfile + self.convertToStars = convertToStars + self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts + } + + static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(GiftViewSheetContent( + context: context.component.context, + subject: context.component.subject, + cancel: { animate in + if animate { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + }, + openPeer: context.component.openPeer, + updateSavedToProfile: context.component.updateSavedToProfile, + convertToStars: context.component.convertToStars, + openStarsIntro: context.component.openStarsIntro, + sendGift: context.component.sendGift, + openMyGifts: context.component.openMyGifts + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + } + } + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + +public class GiftViewScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case message(EngineMessage) + case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) + case soldOutGift(StarGift) + + var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + switch self { + case let .message(message): + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted) + } + case let .profileGift(peerId, gift): + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + case .soldOutGift: + return nil + } + return nil + } + } + + private let context: AccountContext + public var disposed: () -> Void = {} + + private let hapticFeedback = HapticFeedback() + + public init( + context: AccountContext, + subject: GiftViewScreen.Subject, + forceDark: Bool = false, + updateSavedToProfile: ((Bool) -> Void)? = nil, + convertToStars: (() -> Void)? = nil + ) { + self.context = context + + var openPeerImpl: ((EnginePeer) -> Void)? + var updateSavedToProfileImpl: ((Bool) -> Void)? + var convertToStarsImpl: (() -> Void)? + var openStarsIntroImpl: (() -> Void)? + var sendGiftImpl: ((EnginePeer.Id) -> Void)? + var openMyGiftsImpl: (() -> Void)? + + super.init( + context: context, + component: GiftViewSheetComponent( + context: context, + subject: subject, + openPeer: { peerId in + openPeerImpl?(peerId) + }, + updateSavedToProfile: { added in + updateSavedToProfileImpl?(added) + }, + convertToStars: { + convertToStarsImpl?() + }, + openStarsIntro: { + openStarsIntroImpl?() + }, + sendGift: { peerId in + sendGiftImpl?(peerId) + }, + openMyGifts: { + openMyGiftsImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + + openPeerImpl = { [weak self] peer in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + self.dismissAllTooltips() + + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + updateSavedToProfileImpl = { [weak self] added in + guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else { + return + } + if let updateSavedToProfile { + updateSavedToProfile(added) + } else { + let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added) + |> deliverOnMainQueue).startStandalone() + } + + self.dismissAnimated() + + let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText + if let navigationController = self.navigationController as? NavigationController { + Queue.mainQueue().after(0.5) { + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { [weak navigationController] action in + if case .undo = action, let navigationController { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller, animated: true) + } + }) + } + return true + } + ) + lastController.present(resultController, in: .window(.root)) + } + } + } + } + + convertToStarsImpl = { [weak self] in + guard let self, let arguments = subject.arguments, let messageId = arguments.messageId, let fromPeerName = arguments.fromPeerName, let navigationController = self.navigationController as? NavigationController else { + return + } + + let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + if currentTime > starsConvertMaxDate { + //TODO:localize + + let duration: String + if starsConvertMaxDate == 300 { + duration = "5 minutes" + } else { + duration = "90 days" + } + + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Convert_Title, + text: "Sorry, you can't convert this gift.\n\nStars can only be claimed within \(duration) after receiving a gift.", + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + } else { + let delta = starsConvertMaxDate - currentTime + let duration: String + if starsConvertMaxDate == 300 { + duration = "\(delta / 60) minutes" + } else { + duration = "\(delta / 86400) days" + } + //TODO:localize + let text = "Do you want to convert this gift from **\(fromPeerName)** to **\(presentationData.strings.Gift_Convert_Stars(Int32(arguments.convertStars)))**?\n\nConversion is available for the next \(duration).\n\nThis will permanently destroy the gift." + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Convert_Title, + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak navigationController] in + if let convertToStars { + convertToStars() + } else { + let _ = (context.engine.payments.convertStarGift(messageId: messageId) + |> deliverOnMainQueue).startStandalone() + } + self?.dismissAnimated() + + if let navigationController { + Queue.mainQueue().after(0.5) { + if let starsContext = context.starsContext { + navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) + } + + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: presentationData.strings.Gift_Convert_Success_Title, + text: presentationData.strings.Gift_Convert_Success_Text(presentationData.strings.Gift_Convert_Success_Text_Stars(Int32(arguments.convertStars))).string, + customUndoText: nil, + timeout: nil + ), + elevatedLayout: lastController is ChatController, + action: { _ in return true} + ) + lastController.present(resultController, in: .window(.root)) + } + } + } + }) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + } + } + openStarsIntroImpl = { [weak self] in + guard let self else { + return + } + let introController = context.sharedContext.makeStarsIntroScreen(context: context) + self.push(introController) + } + sendGiftImpl = { [weak self] peerId in + guard let self else { + return + } + let _ = (context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions) + self.push(controller) + }) + } + openMyGiftsImpl = { [weak self] in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller, animated: true) + } + }) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + public func dismissAnimated() { + self.dismissAllTooltips() + + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + + fileprivate func dismissAllTooltips() { +// self.window?.forEachController({ controller in +// if let controller = controller as? UndoOverlayController { +// controller.dismiss() +// } +// }) +// self.forEachController({ controller in +// if let controller = controller as? UndoOverlayController { +// controller.dismiss() +// } +// return true +// }) + } +} + +private final class TableComponent: CombinedComponent { + class Item: Equatable { + public let id: AnyHashable + public let title: String? + public let component: AnyComponent + public let insets: UIEdgeInsets? + + public init(id: IdType, title: String?, component: AnyComponent, insets: UIEdgeInsets? = nil) { + self.id = AnyHashable(id) + self.title = title + self.component = component + self.insets = insets + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.component != rhs.component { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + + public init(theme: PresentationTheme, items: [Item]) { + self.theme = theme + self.items = items + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + final class State: ComponentState { + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Rectangle.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:] + var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + var i = 0 + for item in context.component.items { + guard let title = item.title else { + i += 1 + continue + } + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren[i] = titleChild + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + i += 1 + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + var innerTotalHeight: CGFloat = 0.0 + + for item in context.component.items { + let insets: UIEdgeInsets + if let customInsets = item.insets { + insets = customInsets + } else { + insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) + } + + var titleHeight: CGFloat = 0.0 + if let titleChild = updatedTitleChildren[i] { + titleHeight = titleChild.size.height + } + + let availableValueWidth: CGFloat + if titleHeight > 0.0 { + availableValueWidth = rightColumnWidth + } else { + availableValueWidth = context.availableSize.width + } + + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + if titleHeight > 0.0 { + innerTotalHeight += rowHeight + } + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + i += 1 + } + + let leftColumnBackground = leftColumnBackground.update( + component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add( + leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + let borderRadius: CGFloat = 5.0 + borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (valueChild, valueInsets) in updatedValueChildren { + let rowHeight = rowHeights[i] ?? 0.0 + + let valueFrame: CGRect + if let titleChild = updatedTitleChildren[i] { + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + context.add(titleChild + .position(titleFrame.center) + ) + valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) + } else { + valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + } + + context.add(valueChild + .position(valueFrame.center) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} + +private final class PeerCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer?) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + let peerName: String + let avatarOverride: AvatarNodeImageOverride? + if let peerValue = component.peer { + peerName = peerValue.compactDisplayTitle + avatarOverride = nil + } else { + peerName = component.strings.Gift_View_HiddenName + avatarOverride = .anonymousSavedMessagesIcon(isColored: true) + } + + let avatarNaturalSize = CGSize(width: 40.0, height: 40.0) + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride) + self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + + public init( + context: AccountContext, + text: String, + color: UIColor + ) { + self.context = context + self.text = text + self.color = color + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 6.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor = component.color.withAlphaComponent(0.1) + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private struct GiftConfiguration { + static var defaultValue: GiftConfiguration { + return GiftConfiguration(convertToStarsPeriod: 90 * 86400) + } + + let convertToStarsPeriod: Int32 + + fileprivate init(convertToStarsPeriod: Int32) { + self.convertToStarsPeriod = convertToStarsPeriod + } + + static func with(appConfiguration: AppConfiguration) -> GiftConfiguration { + if let data = appConfiguration.data { + var convertToStarsPeriod: Int32? + if let value = data["stargifts_convert_period_max"] as? Double { + convertToStarsPeriod = Int32(value) + } + return GiftConfiguration(convertToStarsPeriod: convertToStarsPeriod ?? GiftConfiguration.defaultValue.convertToStarsPeriod) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift index 3f12c31673e..3fa698bbbd1 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift @@ -302,7 +302,7 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre let thumbnail: StickerPackItem? if let thumbnailRep = info.thumbnail { - thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: []), indexKeys: []) + thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), indexKeys: []) } else { thumbnail = entry.firstItem as? StickerPackItem } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/BUILD b/submodules/TelegramUI/Components/InteractiveTextComponent/BUILD index 86bdcf4b766..0b99a7e2b6e 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/BUILD +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 2868bce6050..4a521443bf6 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -112,13 +112,14 @@ private final class InteractiveTextNodeLine { let isTruncated: Bool let isRTL: Bool var strikethroughs: [InteractiveTextNodeStrikethrough] + var underlines: [InteractiveTextNodeStrikethrough] var spoilers: [InteractiveTextNodeSpoiler] var spoilerWords: [InteractiveTextNodeSpoiler] var embeddedItems: [InteractiveTextNodeEmbeddedItem] var attachments: [InteractiveTextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], underlines: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line self.constrainedWidth = constrainedWidth self.frame = frame @@ -129,6 +130,7 @@ private final class InteractiveTextNodeLine { self.isTruncated = isTruncated self.isRTL = isRTL self.strikethroughs = strikethroughs + self.underlines = underlines self.spoilers = spoilers self.spoilerWords = spoilerWords self.embeddedItems = embeddedItems @@ -1452,6 +1454,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: false, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1493,6 +1496,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: isRTL && segment.blockQuote == nil, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1551,6 +1555,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1605,6 +1610,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1736,6 +1742,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) let x = lowerX < upperX ? lowerX : upperX line.strikethroughs.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) + } else if let _ = attributes[NSAttributedString.Key.underlineStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + line.underlines.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) } if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { @@ -2090,6 +2101,14 @@ final class TextContentItem { } } +private let drawUnderlinesManually: Bool = { + if #available(iOS 18.0, *) { + return true + } else { + return false + } +}() + final class TextContentItemLayer: SimpleLayer { final class Params { let item: TextContentItem @@ -2322,6 +2341,46 @@ final class TextContentItemLayer: SimpleLayer { } } + if drawUnderlinesManually { + if !line.strikethroughs.isEmpty { + for strikethrough in line.strikethroughs { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.midY, width: frame.width, height: 1.0)) + } + } + + if !line.underlines.isEmpty { + for strikethrough in line.underlines { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.maxY - 2.0, width: frame.width, height: 1.0)) + } + } + } + if let (additionalTrailingLine, _) = line.additionalTrailingLine { context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent) diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index b10e34b9d13..5e05e9b07aa 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -235,7 +235,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 70f131dbfb8..abea4059e81 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -208,7 +208,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { style: .media, placeholder: .plain(presentationData.strings.MediaPicker_AddCaption), maxLength: Int(self.context.userLimits.maxCaptionLength), - queryTypes: [.mention], + queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, resetInputContents: resetInputContents, nextInputMode: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD index 4b7a198226e..8cc7d5a0d97 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD @@ -10,12 +10,15 @@ swift_library( #"-warnings-as-errors", ], deps = [ + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/AccountContext", ], visibility = [ diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 0cfdd563cf9..415ca72422b 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -2,10 +2,13 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit import TelegramPresentationData import MultilineTextComponent import ListSectionComponent import TextFieldComponent +import LottieComponent +import PlainButtonComponent import AccountContext public final class ListMultilineTextFieldItemComponent: Component { @@ -14,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component { public fileprivate(set) var text: NSAttributedString = NSAttributedString() public fileprivate(set) var isEditing: Bool = false + public var hasTrackingView = false + + public var currentEmojiSuggestion: TextFieldComponent.EmojiSuggestion? + public var dismissedEmojiSuggestionPosition: TextFieldComponent.EmojiSuggestion.Position? + public init() { } } @@ -30,6 +38,11 @@ public final class ListMultilineTextFieldItemComponent: Component { } } + public enum InputMode { + case keyboard + case emoji + } + public enum EmptyLineHandling { case allowed case oneConsecutive @@ -49,10 +62,13 @@ public final class ListMultilineTextFieldItemComponent: Component { public let characterLimit: Int? public let displayCharacterLimit: Bool public let emptyLineHandling: EmptyLineHandling + public let formatMenuAvailability: TextFieldComponent.FormatMenuAvailability public let updated: ((String) -> Void)? public let returnKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)? public let textUpdateTransition: ComponentTransition + public let inputMode: InputMode? + public let toggleInputMode: (() -> Void)? public let tag: AnyObject? public init( @@ -69,10 +85,13 @@ public final class ListMultilineTextFieldItemComponent: Component { characterLimit: Int? = nil, displayCharacterLimit: Bool = false, emptyLineHandling: EmptyLineHandling = .allowed, + formatMenuAvailability: TextFieldComponent.FormatMenuAvailability = .none, updated: ((String) -> Void)? = nil, returnKeyAction: (() -> Void)? = nil, backspaceKeyAction: (() -> Void)? = nil, textUpdateTransition: ComponentTransition = .immediate, + inputMode: InputMode? = nil, + toggleInputMode: (() -> Void)? = nil, tag: AnyObject? = nil ) { self.externalState = externalState @@ -88,10 +107,13 @@ public final class ListMultilineTextFieldItemComponent: Component { self.characterLimit = characterLimit self.displayCharacterLimit = displayCharacterLimit self.emptyLineHandling = emptyLineHandling + self.formatMenuAvailability = formatMenuAvailability self.updated = updated self.returnKeyAction = returnKeyAction self.backspaceKeyAction = backspaceKeyAction self.textUpdateTransition = textUpdateTransition + self.inputMode = inputMode + self.toggleInputMode = toggleInputMode self.tag = tag } @@ -135,9 +157,15 @@ public final class ListMultilineTextFieldItemComponent: Component { if lhs.emptyLineHandling != rhs.emptyLineHandling { return false } + if lhs.formatMenuAvailability != rhs.formatMenuAvailability { + return false + } if (lhs.updated == nil) != (rhs.updated == nil) { return false } + if lhs.inputMode != rhs.inputMode { + return false + } return true } @@ -145,6 +173,8 @@ public final class ListMultilineTextFieldItemComponent: Component { private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() + private var modeSelector: ComponentView? + private let placeholder = ComponentView() private var customPlaceholder: ComponentView? @@ -197,23 +227,60 @@ public final class ListMultilineTextFieldItemComponent: Component { return false } + public var isActive: Bool { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.isActive + } else { + return false + } + } + public func activateInput() { if let textFieldView = self.textField.view as? TextFieldComponent.View { textFieldView.activateInput() } } + public func deactivateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deactivateInput() + } + } + + public func insertText(text: NSAttributedString) { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.insertText(text) + } + } + + public func backwardsDeleteText() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deleteBackward() + } + } + + public var textFieldView: TextFieldComponent.View? { + return self.textField.view as? TextFieldComponent.View + } + func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state let verticalInset: CGFloat = 12.0 - let sideInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + var rightInset: CGFloat = 16.0 + let modeSelectorSize = CGSize(width: 32.0, height: 32.0) + + if component.inputMode != nil { + rightInset += 34.0 + } let textLimitFont = Font.regular(15.0) var measureTextLimitInset: CGFloat = 0.0 @@ -258,8 +325,8 @@ public final class ListMultilineTextFieldItemComponent: Component { fontSize: 17.0, textColor: component.theme.list.itemPrimaryTextColor, accentColor: component.theme.list.itemPrimaryTextColor, - insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0 + measureTextLimitInset), - hideKeyboard: false, + insets: UIEdgeInsets(top: verticalInset, left: leftInset - 8.0, bottom: verticalInset, right: rightInset - 8.0 + measureTextLimitInset), + hideKeyboard: component.inputMode == .emoji, customInputView: nil, resetText: component.resetText.flatMap { resetText in return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) @@ -267,7 +334,7 @@ public final class ListMultilineTextFieldItemComponent: Component { isOneLineWhenUnfocused: false, characterLimit: component.characterLimit, emptyLineHandling: mappedEmptyLineHandling, - formatMenuAvailability: .none, + formatMenuAvailability: component.formatMenuAvailability, returnKeyType: component.returnKeyType, lockedFormatAction: { }, @@ -309,9 +376,9 @@ public final class ListMultilineTextFieldItemComponent: Component { text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) - let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize) + let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize) if let placeholderView = self.placeholder.view { if placeholderView.superview == nil { placeholderView.layer.anchorPoint = CGPoint() @@ -329,6 +396,9 @@ public final class ListMultilineTextFieldItemComponent: Component { component.externalState?.hasText = self.textFieldExternalState.hasText component.externalState?.text = self.textFieldExternalState.text component.externalState?.isEditing = self.textFieldExternalState.isEditing + component.externalState?.currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion + component.externalState?.dismissedEmojiSuggestionPosition = self.textFieldExternalState.dismissedEmojiSuggestionPosition + component.externalState?.hasTrackingView = self.textFieldExternalState.hasTrackingView var displayRemainingLimit: Int? if let characterLimit = component.characterLimit, component.displayCharacterLimit { @@ -357,7 +427,7 @@ public final class ListMultilineTextFieldItemComponent: Component { environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - sideInset, y: verticalInset + 2.0), size: textLimitLabelSize) + let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - leftInset + 5.0, y: verticalInset + 2.0), size: textLimitLabelSize) if let textLimitLabelView = textLimitLabel.view { if textLimitLabelView.superview == nil { textLimitLabelView.isUserInteractionEnabled = false @@ -374,6 +444,91 @@ public final class ListMultilineTextFieldItemComponent: Component { } } + if let inputMode = component.inputMode { + var modeSelectorTransition = transition + let modeSelector: ComponentView + if let current = self.modeSelector { + modeSelector = current + } else { + modeSelectorTransition = modeSelectorTransition.withAnimation(.none) + modeSelector = ComponentView() + self.modeSelector = modeSelector + } + let animationName: String + var playAnimation = false + if let previousComponent, let previousInputMode = previousComponent.inputMode { + if previousInputMode != inputMode { + playAnimation = true + } + } + switch inputMode { + case .keyboard: + animationName = "input_anim_keyToSmile" + case .emoji: + animationName = "input_anim_smileToKey" + } + + let _ = modeSelector.update( + transition: modeSelectorTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + color: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0), + size: modeSelectorSize + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.toggleInputMode?() + }, + animateScale: false + )), + environment: {}, + containerSize: modeSelectorSize + ) + let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) + if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + + if modeSelectorView.superview == nil { + self.addSubview(modeSelectorView) + ComponentTransition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) + ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001) + } + + if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { + animationView.playOnce() + } + + modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) + modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) + + if let externalState = component.externalState { + let displaySelector = externalState.isEditing + + alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) + alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) + } + } + } else if let modeSelector = self.modeSelector { + self.modeSelector = nil + if let modeSelectorView = modeSelector.view { + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in + modeSelectorView?.removeFromSuperview() + }) + alphaTransition.setScale(view: modeSelectorView, scale: 0.001) + } else { + modeSelectorView.removeFromSuperview() + } + } + } + return size } diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index b8092ada4fb..8f3c68384cd 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -41,17 +41,20 @@ public final class ListSectionContentView: UIView { public final class Configuration { public let theme: PresentationTheme + public let isModal: Bool public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool public let background: ListSectionComponent.Background public init( theme: PresentationTheme, + isModal: Bool = false, displaySeparators: Bool, extendsItemHighlightToSection: Bool, background: ListSectionComponent.Background ) { self.theme = theme + self.isModal = isModal self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection self.background = background @@ -116,7 +119,7 @@ public final class ListSectionContentView: UIView { backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor } else { transition = .easeInOut(duration: 0.2) - backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + backgroundColor = configuration.isModal ? configuration.theme.list.itemModalBlocksBackgroundColor : configuration.theme.list.itemBlocksBackgroundColor } self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) @@ -144,7 +147,7 @@ public final class ListSectionContentView: UIView { if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection { backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor } else { - backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + backgroundColor = configuration.isModal ? configuration.theme.list.itemModalBlocksBackgroundColor : configuration.theme.list.itemBlocksBackgroundColor } self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) @@ -305,6 +308,7 @@ public final class ListSectionComponent: Component { public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] + public let isModal: Bool public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool @@ -314,6 +318,7 @@ public final class ListSectionComponent: Component { header: AnyComponent?, footer: AnyComponent?, items: [AnyComponentWithIdentity], + isModal: Bool = false, displaySeparators: Bool = true, extendsItemHighlightToSection: Bool = false ) { @@ -322,6 +327,7 @@ public final class ListSectionComponent: Component { self.header = header self.footer = footer self.items = items + self.isModal = isModal self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection } @@ -342,6 +348,9 @@ public final class ListSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.isModal != rhs.isModal { + return false + } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -448,6 +457,7 @@ public final class ListSectionComponent: Component { let contentResult = self.contentView.update( configuration: ListSectionContentView.Configuration( theme: component.theme, + isModal: component.isModal, displaySeparators: component.displaySeparators, extendsItemHighlightToSection: component.extendsItemHighlightToSection, background: component.background @@ -522,17 +532,20 @@ public final class ListSubSectionComponent: Component { public let theme: PresentationTheme public let leftInset: CGFloat public let items: [AnyComponentWithIdentity] + public let isModal: Bool public let displaySeparators: Bool public init( theme: PresentationTheme, leftInset: CGFloat, items: [AnyComponentWithIdentity], + isModal: Bool = false, displaySeparators: Bool = true ) { self.theme = theme self.leftInset = leftInset self.items = items + self.isModal = isModal self.displaySeparators = displaySeparators } @@ -546,6 +559,9 @@ public final class ListSubSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.isModal != rhs.isModal { + return false + } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -615,6 +631,7 @@ public final class ListSubSectionComponent: Component { let contentResult = self.contentView.update( configuration: ListSectionContentView.Configuration( theme: component.theme, + isModal: component.isModal, displaySeparators: component.displaySeparators, extendsItemHighlightToSection: false, background: .none(clipped: false) diff --git a/submodules/TelegramUI/Components/LottieMetal/BUILD b/submodules/TelegramUI/Components/LottieMetal/BUILD index 6b3709b2f40..86aedaa5066 100644 --- a/submodules/TelegramUI/Components/LottieMetal/BUILD +++ b/submodules/TelegramUI/Components/LottieMetal/BUILD @@ -48,7 +48,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], data = [ ":LottieMetalSourcesBundle", diff --git a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD index 0fc27dc460f..4546ffc9a2f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD +++ b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index f1906350018..482d271297f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -119,7 +119,7 @@ public final class DrawingMessageRenderer { let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) let _ = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) - Queue.mainQueue().after(0.2, { + Queue.mainQueue().after(0.35, { var mediaRect: CGRect? if let messageNode = self.messageNodes?.first { if self.isOverlay { @@ -343,7 +343,7 @@ public final class DrawingMessageRenderer { } public func render(completion: @escaping (Result) -> Void) { - Queue.mainQueue().after(0.12) { + Queue.mainQueue().justDispatch { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let defaultPresentationData = defaultPresentationData() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 09b076bbd25..4b7755eda93 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -628,10 +628,20 @@ public final class MediaEditor { case let .asset(asset): textureSource = Signal { subscriber in let isVideo = asset.mediaType == .video - + let targetSize = isVideo ? CGSize(width: 128.0, height: 128.0) : CGSize(width: 1920.0, height: 1920.0) let options = PHImageRequestOptions() - options.deliveryMode = isVideo ? .fastFormat : .highQualityFormat + let deliveryMode: PHImageRequestOptionsDeliveryMode + if isVideo { + if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited { + deliveryMode = .highQualityFormat + } else { + deliveryMode = .fastFormat + } + } else { + deliveryMode = .highQualityFormat + } + options.deliveryMode = deliveryMode options.isNetworkAccessAllowed = true let requestId = PHImageManager.default().requestImage( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 9b259b362cb..aee5cbbcb91 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -87,7 +87,7 @@ public extension MediaEditorScreen { if cover, case let .file(file) = storyItem.media { videoPlaybackPosition = 0.0 for attribute in file.attributes { - if case let .Video(_, _, _, _, coverTime) = attribute { + if case let .Video(_, _, _, _, coverTime, _) = attribute { videoPlaybackPosition = coverTime break } @@ -258,8 +258,8 @@ public extension MediaEditorScreen { if case let .file(file) = storyItem.media { var updatedAttributes: [TelegramMediaFileAttribute] = [] for attribute in file.attributes { - if case let .Video(duration, size, flags, preloadSize, _) = attribute { - updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp))) + if case let .Video(duration, size, flags, preloadSize, _, videoCodec) = attribute { + updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp), videoCodec: videoCodec)) } else { updatedAttributes.append(attribute) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 0adc9b1edd9..159e2561630 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1229,7 +1229,7 @@ final class MediaEditorScreenComponent: Component { style: .editor, placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), maxLength: Int(component.context.userLimits.maxStoryCaptionLength), - queryTypes: [.mention], + queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, resetInputContents: nil, nextInputMode: { _ in return nextInputMode }, @@ -1363,12 +1363,12 @@ final class MediaEditorScreenComponent: Component { header: header, isChannel: false, storyItem: nil, - chatLocation: nil + chatLocation: controller.customTarget.flatMap { .peer(id: $0) } )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) - + if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { Queue.mainQueue().justDispatch { controller.node.entitiesView.selectEntity(nil) @@ -8305,7 +8305,7 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel fileAttributes.append(.FileName(fileName: isVideo ? "sticker.webm" : "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) if isVideo { - fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } else { fileAttributes.append(.ImageSize(size: dimensions)) } @@ -8314,7 +8314,7 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes, alternativeRepresentations: []) } private struct MediaEditorConfiguration { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index 67da5b8fe1c..d14432ef69d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -60,7 +60,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu if let resource = thumbnailResource as? CloudDocumentMediaResource { resourceId = resource.fileId } - let thumbnailFile = topItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: []) + let thumbnailFile = topItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: [], alternativeRepresentations: []) let _ = freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start() thumbnailIconSource = ContextMenuActionItemIconSource( diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 634c62295cb..956f088e413 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -11,14 +11,18 @@ import PeerListItemComponent final class ContextResultPanelComponent: Component { enum Results: Equatable { case mentions([EnginePeer]) - case hashtags([String]) + case hashtags(EnginePeer?, [String], String) var count: Int { switch self { - case let .hashtags(hashtags): - return hashtags.count case let .mentions(peers): return peers.count + case let .hashtags(peer, hashtags, query): + var count = hashtags.count + if let _ = peer, query.count >= 4 { + count += 2 + } + return count } } } @@ -186,38 +190,23 @@ final class ContextResultPanelComponent: Component { let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0) -// var synchronousLoad = false -// if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) { -// synchronousLoad = hint.synchronousLoad -// } - var validIds: [AnyHashable] = [] - if let range = itemLayout.visibleItems(for: visibleBounds), case let .mentions(peers) = component.results { + if let range = itemLayout.visibleItems(for: visibleBounds) { for index in range.lowerBound ..< range.upperBound { - guard index < peers.count else { + guard index < component.results.count else { continue } let itemFrame = itemLayout.itemFrame(for: index) - var itemTransition = transition - let peer = peers[index] - validIds.append(peer.id) + let id: AnyHashable - let visibleItem: ComponentView - if let current = self.visibleItems[peer.id] { - visibleItem = current - } else { - if !transition.animation.isImmediate { - itemTransition = .immediate - } - visibleItem = ComponentView() - self.visibleItems[peer.id] = visibleItem - } - - let _ = visibleItem.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( + let itemComponent: AnyComponent + switch component.results { + case let .mentions(peers): + let peer = peers[index] + id = peer.id + itemComponent = AnyComponent(PeerListItemComponent( context: component.context, theme: component.theme, strings: component.strings, @@ -236,21 +225,100 @@ final class ContextResultPanelComponent: Component { } component.action(.mention(peer)) } - )), + )) + case let .hashtags(peer, hashtags, query): + var hashtagIndex = index + if let _ = peer, query.count >= 4 { + hashtagIndex -= 2 + } + + if let peer, let addressName = peer.addressName, hashtagIndex < 0 { + //TODO: localize + var isGroup = false + if case let .channel(channel) = peer, case .group = channel.info { + isGroup = true + } + id = hashtagIndex + if hashtagIndex == -2 { + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "Use #\(query)", + subtitle: "searches posts from all channels", + hashtag: query, + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag(query)) + } + )) + } else { + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: peer, + title: "Use #\(query)@\(addressName)", + subtitle: isGroup ? "searches only posts from this group" : "searches only posts from this channel", + hashtag: "\(query)@\(addressName)", + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag("\(query)@\(addressName)")) + } + )) + } + } else { + let hashtag = hashtags[hashtagIndex] + id = hashtag + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "#\(hashtag)", + subtitle: nil, + hashtag: hashtag, + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag(hashtag)) + } + )) + } + } + validIds.append(id) + + let visibleItem: ComponentView + if let current = self.visibleItems[id] { + visibleItem = current + } else { + if !transition.animation.isImmediate { + itemTransition = .immediate + } + visibleItem = ComponentView() + self.visibleItems[id] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: itemComponent, environment: {}, containerSize: itemFrame.size ) if let itemView = visibleItem.view { -// var animateIn = false if itemView.superview == nil { -// animateIn = true self.scrollView.addSubview(itemView) } itemTransition.setFrame(view: itemView, frame: itemFrame) - -// if animateIn { -// itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) -// } } } } @@ -284,9 +352,10 @@ final class ContextResultPanelComponent: Component { let sideInset: CGFloat = 3.0 self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition) - let measureItemSize = self.measureItem.update( - transition: .immediate, - component: AnyComponent(PeerListItemComponent( + let itemComponent: AnyComponent + switch component.results { + case .mentions: + itemComponent = AnyComponent(PeerListItemComponent( context: component.context, theme: component.theme, strings: component.strings, @@ -301,7 +370,25 @@ final class ContextResultPanelComponent: Component { hasNext: true, action: { _, _, _ in } - )), + )) + case .hashtags: + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "AAAAAAAAAAAA", + subtitle: nil, + hashtag: "", + hasNext: true, + action: { _, _ in + } + )) + } + + let measureItemSize = self.measureItem.update( + transition: .immediate, + component: itemComponent, environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift new file mode 100644 index 00000000000..ed641defc1d --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift @@ -0,0 +1,509 @@ +import Foundation +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode +import TelegramStringFormatting +import AppBundle +import PeerPresenceStatusManager +import EmojiStatusComponent +import ContextUI +import EmojiTextAttachmentView +import TextFormat +import PhotoResources +import ListSectionComponent +import ListItemSwipeOptionContainer + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +public final class HashtagListItemComponent: Component { + public final class TransitionHint { + public let synchronousLoad: Bool + + public init(synchronousLoad: Bool) { + self.synchronousLoad = synchronousLoad + } + } + + public final class InlineAction: Equatable { + public enum Color: Equatable { + case destructive + } + + public let id: AnyHashable + public let title: String + public let color: Color + public let action: () -> Void + + public init(id: AnyHashable, title: String, color: Color, action: @escaping () -> Void) { + self.id = id + self.title = title + self.color = color + self.action = action + } + + public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + } + + public final class InlineActionsState: Equatable { + public let actions: [InlineAction] + + public init(actions: [InlineAction]) { + self.actions = actions + } + + public static func ==(lhs: InlineActionsState, rhs: InlineActionsState) -> Bool { + if lhs === rhs { + return true + } + if lhs.actions != rhs.actions { + return false + } + return true + } + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + let title: String + let subtitle: String? + let hashtag: String + let hasNext: Bool + let action: (String, HashtagListItemComponent.View) -> Void + let contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)? + let inlineActions: InlineActionsState? + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer?, + title: String, + subtitle: String?, + hashtag: String, + hasNext: Bool, + action: @escaping (String, HashtagListItemComponent.View) -> Void, + contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, + inlineActions: InlineActionsState? = nil + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.title = title + self.subtitle = subtitle + self.hashtag = hashtag + self.hasNext = hasNext + self.action = action + self.contextAction = contextAction + self.inlineActions = inlineActions + } + + public static func ==(lhs: HashtagListItemComponent, rhs: HashtagListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.hashtag != rhs.hashtag { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + if lhs.inlineActions != rhs.inlineActions { + return false + } + return true + } + + public final class View: ContextControllerSourceView, ListSectionComponent.ChildView { + public let extractedContainerView: ContextExtractedContentContainingView + private let containerButton: HighlightTrackingButton + + private let swipeOptionContainer: ListItemSwipeOptionContainer + + private let iconBackgroundLayer = SimpleLayer() + private let iconLayer = SimpleLayer() + + private let title = ComponentView() + private var label = ComponentView() + private let separatorLayer: SimpleLayer + private var avatarNode: AvatarNode? + + private let badgeBackgroundLayer = SimpleLayer() + + private var component: HashtagListItemComponent? + private weak var state: EmptyComponentState? + + public var avatarFrame: CGRect { + if let avatarNode = self.avatarNode { + return avatarNode.frame + } else { + return CGRect(origin: CGPoint(), size: CGSize()) + } + } + + public var titleFrame: CGRect? { + return self.title.view?.frame + } + + public var labelFrame: CGRect? { + guard let value = self.label.view?.frame else { + return nil + } + return value + } + + private var isExtractedToContextMenu: Bool = false + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.iconBackgroundLayer.cornerRadius = 15.0 + self.badgeBackgroundLayer.cornerRadius = 4.0 + + self.extractedContainerView = ContextExtractedContentContainingView() + self.containerButton = HighlightTrackingButton() + self.containerButton.layer.anchorPoint = CGPoint() + self.containerButton.isExclusiveTouch = true + + self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect()) + + super.init(frame: frame) + + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer) + + self.swipeOptionContainer.addSubview(self.containerButton) + + self.layer.addSublayer(self.separatorLayer) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self else { + return + } + + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = nil + self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + } + self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in + guard let self else { + return + } + self.isExtractedToContextMenu = value + + let mappedTransition: ComponentTransition + if value { + mappedTransition = ComponentTransition(transition) + } else { + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.state?.updated(transition: mappedTransition) + } + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(component.hashtag, self.extractedContainerView, gesture) + } + + self.containerButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(highlighted) + } + } + + self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in + guard let self else { + return + } + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) + } + self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in + guard let self, let component = self.component else { + return + } + guard let inlineActions = component.inlineActions else { + return + } + self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) + if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) { + inlineAction.action() + } + } + + self.containerButton.layer.addSublayer(self.iconBackgroundLayer) + self.iconBackgroundLayer.addSublayer(self.iconLayer) + + self.containerButton.layer.addSublayer(self.badgeBackgroundLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action("\(component.hashtag) ", self) + } + + func update(component: HashtagListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + var synchronousLoad = false + if let hint = transition.userData(TransitionHint.self) { + synchronousLoad = hint.synchronousLoad + } + + self.isGestureEnabled = false + + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let labelData: (String, UIColor) + if let subtitle = component.subtitle { + labelData = (subtitle, component.theme.list.itemSecondaryTextColor) + } else { + labelData = ("", .clear) + } + + let contextInset: CGFloat + if self.isExtractedToContextMenu { + contextInset = 12.0 + } else { + contextInset = 0.0 + } + + let height: CGFloat = 42.0 + let titleFont: UIFont = Font.semibold(14.0) + let subtitleFont: UIFont = Font.regular(14.0) + + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 55.0 + let rightInset: CGFloat = 16.0 + + let avatarSize: CGFloat = 30.0 + let avatarFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + + if let peer = component.peer { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarFont) + avatarNode.isLayerBacked = false + avatarNode.isUserInteractionEnabled = false + self.avatarNode = avatarNode + self.containerButton.layer.insertSublayer(avatarNode.layer, at: 0) + } + + if avatarNode.bounds.isEmpty { + avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: avatarNode.layer, frame: avatarFrame) + } + + if peer.smallProfileImage != nil { + avatarNode.setPeerV2( + context: component.context, + theme: component.theme, + peer: peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: synchronousLoad, + displayDimensions: CGSize(width: avatarSize, height: avatarSize) + ) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: .round, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + self.iconBackgroundLayer.isHidden = true + } else { + self.iconBackgroundLayer.isHidden = false + } + + let previousTitleFrame = self.title.view?.frame + + let titleAvailableWidth = availableSize.width - leftInset - rightInset + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: titleFont, textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: titleAvailableWidth, height: 100.0) + ) + + let labelAvailableWidth = availableSize.width - leftInset - rightInset + let labelColor: UIColor = labelData.1 + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelColor)) + )), + environment: {}, + containerSize: CGSize(width: labelAvailableWidth, height: 100.0) + ) + + let titleVerticalOffset: CGFloat = 0.0 + let centralContentHeight: CGFloat + if labelSize.height > 0.0 { + centralContentHeight = titleSize.height + labelSize.height + } else { + centralContentHeight = titleSize.height + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalOffset + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + if let labelView = self.label.view { + let labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY), size: labelSize) + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + labelView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(labelView) + + labelView.center = labelFrame.origin + } else { + transition.setPosition(view: labelView, position: labelFrame.origin) + } + + labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) + } + + if self.iconLayer.contents == nil { + self.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage + } + + if themeUpdated { + let accentColor = UIColor(rgb: 0x007aff) + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + self.iconBackgroundLayer.backgroundColor = accentColor.cgColor + self.iconLayer.layerTintColor = UIColor.white.cgColor + self.badgeBackgroundLayer.backgroundColor = accentColor.cgColor + } + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let iconSize = CGSize(width: 30.0, height: 30.0) + self.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - 30.0) / 2.0)), size: iconSize) + self.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0)) + + let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.extractedContainerView, frame: resultBounds) + transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds) + self.extractedContainerView.contentRect = resultBounds + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + + let swipeOptionContainerFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame) + + transition.setPosition(view: self.containerButton, position: containerFrame.origin) + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerFrame.size)) + + self.separatorInset = leftInset + + self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) + + var rightOptions: [ListItemSwipeOptionContainer.Option] = [] + if let inlineActions = component.inlineActions { + rightOptions = inlineActions.actions.map { action in + let color: UIColor + let textColor: UIColor + switch action.color { + case .destructive: + color = component.theme.list.itemDisclosureActions.destructive.fillColor + textColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor + } + + return ListItemSwipeOptionContainer.Option( + key: action.id, + title: action.title, + icon: .none, + color: color, + textColor: textColor + ) + } + } + self.swipeOptionContainer.setRevealOptions(([], rightOptions)) + + return CGSize(width: availableSize.width, height: height) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index 3051274d1a4..4dd642f7f4a 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -130,10 +130,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha case .hashtag: break default: - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } } else { - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags() @@ -145,7 +145,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha result.append(hashtag) } } - return { _ in return .hashtags(result) } + return { _ in return .hashtags(result, query) } } |> castError(ChatContextQueryError.self) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 3c678b46222..b481ee2ecf7 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -472,6 +472,7 @@ public final class MessageInputPanelComponent: Component { private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:] + private var contextQueryPeer: EnginePeer? private var contextQueryResultPanel: ComponentView? private var stickersResultPanel: ComponentView? @@ -643,6 +644,17 @@ public final class MessageInputPanelComponent: Component { } let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates) + if self.contextQueryPeer == nil, let peerId = component.chatLocation?.peerId { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, peer?.addressName != nil else { + return + } + self.contextQueryPeer = peer + self.state?.updated(transition: .immediate) + }) + } + for (kind, update) in contextQueryUpdates { switch update { case .remove: @@ -1871,6 +1883,8 @@ public final class MessageInputPanelComponent: Component { var contextResults: ContextResultPanelComponent.Results? if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty { contextResults = .mentions(mentions) + } else if let result = self.contextQueryResults[.hashtag], case let .hashtags(hashtags, query) = result, !hashtags.isEmpty || (query.count >= 4 && self.contextQueryPeer != nil) { + contextResults = .hashtags(self.contextQueryPeer, hashtags, query) } if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty { @@ -1989,7 +2003,23 @@ public final class MessageInputPanelComponent: Component { } } case let .hashtag(hashtag): - let _ = hashtag + var hashtagQueryRange: NSRange? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) { + if type == [.hashtag] { + hashtagQueryRange = range + break inner + } + } + + if let range = hashtagQueryRange { + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + let replacementText = hashtag + inputText.replaceCharacters(in: range, with: replacementText) + + let selectionPosition = range.lowerBound + (replacementText as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } } } } diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/BUILD b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD index 02745a260c5..fa3fb40677d 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/BUILD +++ b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Display", diff --git a/submodules/TelegramUI/Components/MinimizedContainer/BUILD b/submodules/TelegramUI/Components/MinimizedContainer/BUILD index 514280d85b5..9e40d91e9c5 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/BUILD +++ b/submodules/TelegramUI/Components/MinimizedContainer/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/BUILD b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD index 775f9ca233d..04114e3a808 100644 --- a/submodules/TelegramUI/Components/NavigationStackComponent/BUILD +++ b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Display", diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift index e7a795459fc..3a5aabd841c 100644 --- a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -85,13 +85,16 @@ public final class NavigationStackComponent: Compon } public let items: [AnyComponentWithIdentity] + public let clipContent: Bool public let requestPop: () -> Void public init( items: [AnyComponentWithIdentity], + clipContent: Bool = true, requestPop: @escaping () -> Void ) { self.items = items + self.clipContent = clipContent self.requestPop = requestPop } @@ -99,6 +102,9 @@ public final class NavigationStackComponent: Compon if lhs.items != rhs.items { return false } + if lhs.clipContent != rhs.clipContent { + return false + } return true } @@ -198,7 +204,7 @@ public final class NavigationStackComponent: Compon } else { itemTransition = itemTransition.withAnimation(.none) itemView = ItemView() - itemView.clipsToBounds = true + itemView.clipsToBounds = component.clipContent self.itemViews[itemId] = itemView itemView.contents.parentState = state } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 539f4497e5c..5fa8c37f6fa 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -52,6 +52,23 @@ final class PeerAllowedReactionsScreenComponent: Component { return true } + private struct EmojiSearchResult { + var groups: [EmojiPagerContentComponent.ItemGroup] + var id: AnyHashable + var version: Int + var isPreset: Bool + } + + private struct EmojiSearchState { + var result: EmojiSearchResult? + var isSearching: Bool + + init(result: EmojiSearchResult?, isSearching: Bool) { + self.result = result + self.isSearching = isSearching + } + } + final class View: UIView, UIScrollViewDelegate { private let scrollView: UIScrollView private let switchItem = ComponentView() @@ -84,6 +101,19 @@ final class PeerAllowedReactionsScreenComponent: Component { private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? + + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { + didSet { + self.emojiSearchState.set(.single(self.emojiSearchStateValue)) + } + } + + private var emptyResultEmojis: [TelegramMediaFile] = [] + private var stableEmptyResultEmoji: TelegramMediaFile? + private let stableEmptyResultEmojiDisposable = MetaDisposable() + private var caretPosition: Int? private var displayInput: Bool = false @@ -410,15 +440,43 @@ final class PeerAllowedReactionsScreenComponent: Component { chatPeerId: nil, selectedItems: Set(), backgroundIconColor: nil, - hasSearch: false, + hasSearch: true, forceHasPremium: true ) - self.emojiContentDisposable = (emojiContent - |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), + emojiContent, + self.emojiSearchState.get() + ) + |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in guard let self else { return } - self.emojiContent = emojiContent + guard let environment = self.environment else { + return + } + + var emojiContent = emojiContent + if let emojiSearchResult = emojiSearchState.result { + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + if self.stableEmptyResultEmoji == nil { + self.stableEmptyResultEmoji = self.emptyResultEmojis.randomElement() + } + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: environment.strings.EmojiSearch_SearchReactionsEmptyResult, + iconFile: self.stableEmptyResultEmoji + ) + } else { + self.stableEmptyResultEmoji = nil + } + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) + } else { + self.stableEmptyResultEmoji = nil + + if emojiSearchState.isSearching { + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching) + } + } emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in @@ -531,7 +589,365 @@ final class PeerAllowedReactionsScreenComponent: Component { }, requestUpdate: { _ in }, - updateSearchQuery: { _ in + updateSearchQuery: { [weak self] query in + guard let self, let component = self.component else { + return + } + + switch query { + case .none: + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + } else { + let context = component.context + let isEmojiOnly = !"".isEmpty + + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> + do { + let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) + |> then( + context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { + ($0, true) + } + ) + let localPacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) + + resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + if isEmojiOnly { + var items: [EmojiPagerContentComponent.Item] = [] + for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { + for emojiString in list { + if allEmoticons[emojiString] != nil { + let item = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + items.append(item) + } + } + } + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )) + return .single(resultGroups) + } else { + let remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys)) + + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), + context.engine.stickers.availableReactions() |> take(1), + hasPremium |> take(1), + remotePacksSignal, + remoteSignal, + localPacksSignal + ) + |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for itemFile in foundEmoji.items { + for attribute in itemFile.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, itemFile, keyword)) + } else if alt == query { + result.append((alt, itemFile, alt)) + } + default: + break + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !item.file.isPremiumEmoji { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, item.file, keyword)) + } else if alt == query { + result.append((alt, item.file, alt)) + } + } + default: + break + } + } + } + + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in result { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: (!hasPremium && itemFile.isPremiumEmoji) ? .locked : .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + } + + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )) + + var combinedSets: FoundStickerSets + combinedSets = foundLocalPacks + combinedSets = combinedSets.merge(with: foundPacks.sets) + + var existingCollectionIds = Set() + for (collectionId, info, _, _) in combinedSets.infos { + if !existingCollectionIds.contains(collectionId) { + existingCollectionIds.insert(collectionId) + } else { + continue + } + + if let info = info as? StickerPackCollectionInfo { + var topItems: [StickerPackItem] = [] + for e in combinedSets.entries { + if let item = e.item as? StickerPackItem { + if e.index.collectionId == collectionId { + topItems.append(item) + } + } + } + + var groupItems: [EmojiPagerContentComponent.Item] = [] + for item in topItems { + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: (!hasPremium && item.file.isPremiumEmoji) ? .locked : .none, + tintMode: tintMode + ) + + groupItems.append(resultItem) + } + + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: AnyHashable(info.id), + groupId: AnyHashable(info.id), + title: info.title, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: 3, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: groupItems + )) + } + } + return resultGroups + } + } + } + } + + var version = 0 + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 + })) + } + case let .category(value): + let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> + do { + resultSignal = component.context.engine.stickers.searchEmoji(category: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single(([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )], isFinalResult)) + } + } + + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + //self.emojiSearchStateValue.isSearching = true + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: true, + items: [] + ) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + version += 1 + })) + } }, updateScrollingToItemGroup: { }, @@ -548,6 +964,8 @@ final class PeerAllowedReactionsScreenComponent: Component { addImage: nil ) + self.emojiContent = emojiContent + if !self.isUpdating { self.state?.updated(transition: .immediate) } @@ -936,7 +1354,8 @@ final class PeerAllowedReactionsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(paidReactionsFooterText), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") @@ -1151,6 +1570,7 @@ final class PeerAllowedReactionsScreenComponent: Component { animateIn = true reactionSelectionControl = ComponentView() self.reactionSelectionControl = reactionSelectionControl + reactionSelectionControl.parentState = state } let reactionSelectionControlSize = reactionSelectionControl.update( transition: animateIn ? .immediate : transition, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 0306135d387..785ce6231f9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -10,6 +10,7 @@ public enum PeerInfoPaneKey: Int32 { case members case stories case storyArchive + case gifts case media case savedMessagesChats case savedMessages diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 62c7351e08f..112e1a3f9cf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -160,6 +160,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift index 75dfe458b3d..20641f24281 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift @@ -5,7 +5,7 @@ import TelegramPresentationData import AnimationUI import Display -class DynamicIslandMaskNode: ASDisplayNode { +final class DynamicIslandMaskNode: ASDisplayNode { var animationNode: AnimationNode? var isForum = false { @@ -39,7 +39,7 @@ class DynamicIslandMaskNode: ASDisplayNode { } } -class DynamicIslandBlurNode: ASDisplayNode { +final class DynamicIslandBlurNode: ASDisplayNode { private var effectView: UIVisualEffectView? private let fadeNode = ASDisplayNode() let gradientNode = ASImageNode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 27bcb14a8a3..1759c73b7fe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -182,7 +182,7 @@ public final class LoadingOverlayNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index 991453a8178..2dd9c7b988e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -100,7 +100,6 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode private let listNode: ListView private var currentEntries: [RecommendedChannelsListEntry] = [] private var currentState: (RecommendedChannels?, Bool)? - private var canLoadMore: Bool = false private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] private var unlockBackground: UIImageView? diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index 8922ed2a8d1..a07718b06f8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -327,13 +327,13 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) markupNode.updateVisibility(true) } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 48263e0a887..85ac89458a6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -386,6 +386,9 @@ final class PeerInfoScreenData { let starsRevenueStatsState: StarsRevenueStats? let starsRevenueStatsContext: StarsRevenueStatsContext? let revenueStatsState: RevenueStats? + let revenueStatsContext: RevenueStatsContext? + let profileGiftsContext: ProfileGiftsContext? + let premiumGiftOptions: [PremiumGiftCodeOption] let _isContact: Bool var forceIsContact: Bool = false @@ -431,7 +434,10 @@ final class PeerInfoScreenData { starsState: StarsContext.State?, starsRevenueStatsState: StarsRevenueStats?, starsRevenueStatsContext: StarsRevenueStatsContext?, - revenueStatsState: RevenueStats? + revenueStatsState: RevenueStats?, + revenueStatsContext: RevenueStatsContext?, + profileGiftsContext: ProfileGiftsContext?, + premiumGiftOptions: [PremiumGiftCodeOption] ) { self.peer = peer self.chatPeer = chatPeer @@ -466,6 +472,9 @@ final class PeerInfoScreenData { self.starsRevenueStatsState = starsRevenueStatsState self.starsRevenueStatsContext = starsRevenueStatsContext self.revenueStatsState = revenueStatsState + self.revenueStatsContext = revenueStatsContext + self.profileGiftsContext = profileGiftsContext + self.premiumGiftOptions = premiumGiftOptions } } @@ -958,7 +967,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, starsState: starsState, starsRevenueStatsState: nil, starsRevenueStatsContext: nil, - revenueStatsState: nil + revenueStatsState: nil, + revenueStatsContext: nil, + profileGiftsContext: nil, + premiumGiftOptions: [] ) } } @@ -1003,7 +1015,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsState: nil, starsRevenueStatsState: nil, starsRevenueStatsContext: nil, - revenueStatsState: nil + revenueStatsState: nil, + revenueStatsContext: nil, + profileGiftsContext: nil, + premiumGiftOptions: [] )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1015,6 +1030,23 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon = nil } + let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> + let profileGiftsContext: ProfileGiftsContext? + if case .user = kind { + if isMyProfile || userPeerId != context.account.peerId { + profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId) + } else { + profileGiftsContext = nil + } + premiumGiftOptions = .single([]) + |> then( + context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + ) + } else { + profileGiftsContext = nil + premiumGiftOptions = .single([]) + } + enum StatusInputData: Equatable { case none case presence(TelegramUserPresence) @@ -1240,7 +1272,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in - guard let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal else { + var canViewStarsRevenue = false + if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal { + canViewStarsRevenue = true + } + #if DEBUG + canViewStarsRevenue = "".isEmpty + #endif + + guard canViewStarsRevenue else { return .single((nil, nil)) } let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) @@ -1250,6 +1290,30 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } + let revenueContextAndState = combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) + |> distinctUntilChanged + ) + |> mapToSignal { peer, canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in + var canViewRevenue = canViewRevenue + if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal { + canViewRevenue = true + } + #if DEBUG + canViewRevenue = "".isEmpty + #endif + guard canViewRevenue else { + return .single((nil, nil)) + } + let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId) + return revenueStatsContext.state + |> map { state -> (RevenueStatsContext?, RevenueStats?) in + return (revenueStatsContext, state.stats) + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), @@ -1266,20 +1330,33 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasBotPreviewItems, peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false), privacySettings, - starsRevenueContextAndState + starsRevenueContextAndState, + revenueContextAndState, + premiumGiftOptions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) if let hasStoryArchive, hasStoryArchive { availablePanes?.insert(.storyArchive, at: 1) } + if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData { + if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 { + availablePanes?.insert(.gifts, at: hasStoryArchive == true ? 2 : 1) + } + } } else if let hasStories { if hasStories, peerView.peers[peerView.peerId] is TelegramUser, peerView.peerId != context.account.peerId { availablePanes?.insert(.stories, at: 0) } + if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData, peerView.peerId != context.account.peerId { + if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 { + availablePanes?.insert(.gifts, at: hasStories ? 1 : 0) + } + } + if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { if cachedData.commonGroupCount != 0 { availablePanes?.append(.groupsInCommon) @@ -1373,7 +1450,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsState: nil, starsRevenueStatsState: starsRevenueContextAndState.1, starsRevenueStatsContext: starsRevenueContextAndState.0, - revenueStatsState: nil + revenueStatsState: revenueContextAndState.1, + revenueStatsContext: revenueContextAndState.0, + profileGiftsContext: profileGiftsContext, + premiumGiftOptions: premiumGiftOptions ) } case .channel: @@ -1582,7 +1662,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsState: nil, starsRevenueStatsState: starsRevenueContextAndState.1, starsRevenueStatsContext: starsRevenueContextAndState.0, - revenueStatsState: revenueContextAndState.1 + revenueStatsState: revenueContextAndState.1, + revenueStatsContext: revenueContextAndState.0, + profileGiftsContext: nil, + premiumGiftOptions: [] ) } case let .group(groupId): @@ -1882,7 +1965,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsState: nil, starsRevenueStatsState: nil, starsRevenueStatsContext: nil, - revenueStatsState: nil + revenueStatsState: nil, + revenueStatsContext: nil, + profileGiftsContext: nil, + premiumGiftOptions: [] )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index a2e196d1e4c..9d34dd106ce 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -162,13 +162,13 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { markupNode.removeFromSupernode() } - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) videoNode.isUserInteractionEnabled = false self.videoStartTimestamp = video.representation.startTimestamp self.videoContent = videoContent diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 9776e0ddcbc..589710313b6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1631,7 +1631,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { controlsClippingFrame = apparentAvatarFrame } - let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 48.0 : 0.0 + let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 47.0 : 0.0 let clippingNodeTransition = ContainedViewLayoutTransition.immediate clippingNodeTransition.updateFrame(layer: self.avatarClippingNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: avatarClipOffset), size: CGSize(width: width, height: 1000.0))) clippingNodeTransition.updateSublayerTransformOffset(layer: self.avatarClippingNode.layer, offset: CGPoint(x: 0.0, y: -avatarClipOffset)) @@ -2118,7 +2118,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationTransition.updateAlpha(node: self.buttonsContainerNode, alpha: backgroundBannerAlpha) - let bannerFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentBackgroundHeight), size: CGSize(width: width, height: 2000.0)) + let bannerInset: CGFloat = 3.0 + let bannerFrame = CGRect(origin: CGPoint(x: -bannerInset, y: -2000.0 + apparentBackgroundHeight), size: CGSize(width: width + bannerInset * 2.0, height: 2000.0)) if additive { transition.updateFrameAdditive(view: self.backgroundBannerView, frame: bannerFrame) @@ -2140,16 +2141,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { patternTransitionFraction: buttonsTransitionFraction * backgroundTransitionFraction )), environment: {}, - containerSize: CGSize(width: width, height: apparentBackgroundHeight) + containerSize: CGSize(width: width + bannerInset * 2.0, height: apparentBackgroundHeight + bannerInset) ) if let backgroundCoverView = self.backgroundCover.view { if backgroundCoverView.superview == nil { self.backgroundBannerView.addSubview(backgroundCoverView) } if additive { - transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height), size: backgroundCoverSize)) + transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize)) } else { - transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height), size: backgroundCoverSize)) + transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index dd48610be3b..46f8f53d00c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -13,6 +13,8 @@ import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import PeerInfoChatListPaneNode import PeerInfoChatPaneNode +import TextFormat +import EmojiTextAttachmentView final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey @@ -41,6 +43,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private var iconLayers: [InlineStickerItemLayer] = [] private var isSelected: Bool = false @@ -64,10 +67,46 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { self.pressed() } - func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) { self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) + if !icons.isEmpty { + if self.iconLayers.isEmpty { + for icon in icons { + let iconSize = CGSize(width: 24.0, height: 24.0) + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: icon.fileId.id, + file: icon + ) + + let animationLayer = InlineStickerItemLayer( + context: .account(context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: icon, + cache: context.animationCache, + renderer: context.animationRenderer, + unique: true, + placeholderColor: presentationData.theme.list.mediaPlaceholderColor, + pointSize: iconSize, + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.iconLayers.append(animationLayer) + self.layer.addSublayer(animationLayer) + } + } + } else { + for layer in self.iconLayers { + layer.removeFromSuperlayer() + } + self.iconLayers.removeAll() + } + self.buttonNode.accessibilityLabel = title self.buttonNode.accessibilityTraits = [.button] if isSelected { @@ -76,9 +115,22 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { } func updateLayout(height: CGFloat) -> CGFloat { + var totalWidth: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - return titleSize.width + totalWidth = titleSize.width + + if !self.iconLayers.isEmpty { + totalWidth += 1.0 + let iconSize = CGSize(width: 24.0, height: 24.0) + let spacing: CGFloat = 1.0 + for iconlayer in self.iconLayers { + iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 12.0), size: iconSize) + totalWidth += iconSize.width + spacing + } + totalWidth -= spacing + } + return totalWidth } func updateArea(size: CGSize, sideInset: CGFloat) { @@ -89,6 +141,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { struct PeerInfoPaneSpecifier: Equatable { var key: PeerInfoPaneKey var title: String + var icons: [TelegramMediaFile] } private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { @@ -96,6 +149,7 @@ private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGF } final class PeerInfoPaneTabsContainerNode: ASDisplayNode { + private let context: AccountContext private let scrollNode: ASScrollNode private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] private let selectedLineNode: ASImageNode @@ -104,7 +158,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { var requestSelectPane: ((PeerInfoPaneKey) -> Void)? - override init() { + init(context: AccountContext) { + self.context = context self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() @@ -153,7 +208,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { }) self.paneNodes[specifier.key] = paneNode } - paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) + paneNode.updateText(context: self.context, title: specifier.title, icons: specifier.icons, isSelected: selectedPane == specifier.key, presentationData: presentationData) } var removeKeys: [PeerInfoPaneKey] = [] for (key, _) in self.paneNodes { @@ -407,6 +462,8 @@ private final class PeerInfoPendingPane { var captureProtected = data.peer?.isCopyProtectionEnabled ?? false let paneNode: PeerInfoPaneNode switch key { + case .gifts: + paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, profileGifts: data.profileGiftsContext!) case .stories, .storyArchive, .botPreview: var canManage = false if let peer = data.peer { @@ -596,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode.isUserInteractionEnabled = false - self.tabsContainerNode = PeerInfoPaneTabsContainerNode() + self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context) self.tabsSeparatorNode = ASDisplayNode() self.tabsSeparatorNode.isLayerBacked = true @@ -1120,6 +1177,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String + var icons: [TelegramMediaFile] = [] switch key { case .stories: title = presentationData.strings.PeerInfo_PaneStories @@ -1149,8 +1207,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.DialogList_TabTitle case .savedMessages: title = presentationData.strings.PeerInfo_SavedMessagesTabTitle + case .gifts: + title = presentationData.strings.PeerInfo_PaneGifts + icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).map { $0.gift.file } ?? [] } - return PeerInfoPaneSpecifier(key: key, title: title) + return PeerInfoPaneSpecifier(key: key, title: title, icons: icons) }, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition) for (_, pane) in self.pendingPanes { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index d8d55013382..cdde1baea3b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -591,6 +591,7 @@ private final class PeerInfoInteraction { let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void + let editingOpenRevenue: () -> Void let editingOpenStars: () -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void @@ -662,6 +663,7 @@ private final class PeerInfoInteraction { editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, + editingOpenRevenue: @escaping () -> Void, editingOpenStars: @escaping () -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, @@ -732,6 +734,7 @@ private final class PeerInfoInteraction { self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup + self.editingOpenRevenue = editingOpenRevenue self.editingOpenStars = editingOpenStars self.openParticipantsSection = openParticipantsSection self.openRecentActions = openRecentActions @@ -967,19 +970,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p // MARK: Nicegram AiChat if AiChatUITgHelper.isAiBotFeatureEnabled() { - items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: -1, text: AiChatUITgHelper.botName, icon: PresentationResourcesSettings.aiChatIcon, action: { + items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: -1, text: AiChatUITgHelper.botName, icon: PresentationResourcesSettings.ngAiChatIcon, action: { interaction.openSettings(.nicegramAiChatBot) })) } // if !NGENV.premium_bundle.isEmpty { - items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: ngId, text: l("Premium.Title"), icon: PresentationResourcesSettings.premiumIcon, action: { + items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: ngId, text: l("Premium.Title"), icon: PresentationResourcesSettings.ngPremiumIcon, action: { interaction.openSettings(.nicegramPremium) })) ngId += 1 } - items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: ngId, text: l("AppName"), icon: PresentationResourcesSettings.nicegramIcon, action: { + items[.nicegram]!.append(PeerInfoScreenDisclosureItem(id: ngId, text: l("AppName"), icon: PresentationResourcesSettings.ngSettingsIcon, action: { interaction.openSettings(.nicegram) })) @@ -987,7 +990,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p let getWalletAvailabilityUseCase = WalletContainer.shared.getWalletAvailabilityUseCase() if #available(iOS 15.0, *), getWalletAvailabilityUseCase() { items[.nicegramWallet]?.append(PeerInfoScreenHeaderItem(id: 0, text: "Multichain, Non-custodial")) - items[.nicegramWallet]!.append(PeerInfoScreenDisclosureItem(id: 1, text: "Nicegram Wallet", icon: PresentationResourcesSettings.nicegramIcon, action: { + items[.nicegramWallet]!.append(PeerInfoScreenDisclosureItem(id: 1, text: "Nicegram Wallet", icon: PresentationResourcesSettings.ngWalletIcon, action: { Task { @MainActor in WalletEntryPoints.openHome() } @@ -1104,7 +1107,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) // MARK: Nicegram, comment this item (hide "Gift Premium") /* - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_SendGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) */ @@ -1319,6 +1322,7 @@ private enum InfoSection: Int, CaseIterable { case calls case personalChannel case peerInfo + case balances case peerInfoTrailing case peerMembers } @@ -1512,7 +1516,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) }) } @@ -1632,23 +1637,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } + let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0 + let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0 + let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? 0 let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 - if overallStarsBalance > 0 { - var string = "" - if overallStarsBalance > 0 { - string.append("*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))") + if overallRevenueBalance > 0 || overallStarsBalance > 0 { + //TODO:localize + items[.balances]!.append(PeerInfoScreenHeaderItem(id: 20, text: presentationData.strings.PeerInfo_BotBalance_Title)) + if overallRevenueBalance > 0 { + let string = "*\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))" + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 21, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_BotBalance_Ton, icon: PresentationResourcesSettings.ton, action: { + interaction.editingOpenRevenue() + })) } - let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) - if let range = attributedString.string.range(of: "*") { - attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) - attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + + if overallStarsBalance > 0 { + let string = "*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))" + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 22, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_BotBalance_Stars, icon: PresentationResourcesSettings.stars, action: { + interaction.editingOpenStars() + })) } - - items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 9, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { - interaction.editingOpenStars() - })) + } else { + print() } if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { @@ -1849,7 +1871,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if overallRevenueBalance > 0 || overallStarsBalance > 0 { var string = "" if overallRevenueBalance > 0 { - string.append("#\(formatTonAmountText(revenueBalance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))") + string.append("#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))") } if overallStarsBalance > 0 { if !string.isEmpty { @@ -2967,6 +2989,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenDiscussionGroupSetup: { [weak self] in self?.editingOpenDiscussionGroupSetup() }, + editingOpenRevenue: { [weak self] in + self?.editingOpenRevenue() + }, editingOpenStars: { [weak self] in self?.editingOpenStars() }, @@ -3671,7 +3696,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -3691,6 +3718,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in @@ -5400,7 +5428,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let navigationController = self.controller?.navigationController as? NavigationController else { return } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, message: nil, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, message: nil, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let strongSelf = self else { return } @@ -5446,7 +5474,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, forceUpdate: false, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation) commit() }, sendFile: nil, @@ -5623,11 +5651,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let peerId = self.peerId let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true) - var openUrlImpl: ((String, Bool, @escaping () -> Void) -> Void)? + var openUrlImpl: ((String, Bool, Bool, @escaping () -> Void) -> Void)? var presentImpl: ((ViewController, Any?) -> Void)? - let controller = standaloneWebAppController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { url, concealed, commit in - openUrlImpl?(url, concealed, commit) + let controller = standaloneWebAppController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { url, concealed, forceUpdate, commit in + openUrlImpl?(url, concealed, forceUpdate, commit) }, requestSwitchInline: { _, _, _ in }, getNavigationController: { [weak self] in return (self?.controller?.navigationController as? NavigationController) ?? context.sharedContext.mainWindow?.viewController as? NavigationController @@ -5635,7 +5663,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro controller.navigationPresentation = .flatModal self.controller?.push(controller) - openUrlImpl = { [weak self, weak controller] url, concealed, commit in + openUrlImpl = { [weak self, weak controller] url, concealed, forceUpdate, commit in let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { result in @@ -5645,7 +5673,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else if let current = controller?.navigationController as? NavigationController { navigationController = current } - context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: forceUpdate, openPeer: { peer, navigation in if let navigationController { PeerInfoScreenImpl.openPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) } @@ -6337,8 +6365,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport), let cachedData = data.cachedData as? CachedUserData, !cachedData.premiumGiftOptions.isEmpty { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_GiftPremium, icon: { theme in + if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -6441,7 +6469,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6650,7 +6678,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6780,7 +6808,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6813,6 +6841,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } + var canReport = true + if case .creator = group.role { + canReport = false + } + if canReport { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.ReportPeer_Report, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, f in + self?.openReport(type: .default, contextController: c, backAction: { c in + if let mainItemsImpl = mainItemsImpl { + c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + } + }) + }))) + } + let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: group, chatPeer: group, cachedData: strongSelf.data?.cachedData) if clearPeerHistory.canClearForMyself != nil || clearPeerHistory.canClearForEveryone != nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ClearMessages, icon: { theme in @@ -6911,15 +6955,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - private func openChatForReporting(_ reason: ReportReason) { + private func openChatForReporting(title: String, option: Data, message: String?) { if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) { if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").startStandalone() - - self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + //let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").startStandalone() + //self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) } else { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, reportReason: reason, completion: { _ in - })) + self.context.sharedContext.navigateToChatController( + NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .peer(EnginePeer(peer)), + keepStack: .default, + reportReason: NavigateToChatControllerParams.ReportReason(title: title, option: option, message: message), + completion: { _ in + } + ) + ) } } } @@ -7307,7 +7359,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func scheduleGroupCall() { - self.context.scheduleGroupCall(peerId: self.peerId) + guard let controller = self.controller else { + return + } + self.context.scheduleGroupCall(peerId: self.peerId, parentController: controller) } private func createExternalStream(credentialsPromise: Promise?) { @@ -8532,9 +8587,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openReport(type: PeerInfoReportType, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { - guard let controller = self.controller else { - return - } self.view.endEditing(true) switch type { @@ -8573,19 +8625,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro ]) self.controller?.present(actionSheet, in: .window(.root)) default: - let options: [PeerReportOption] - if case .user = type { - options = [.spam, .fake, .violence, .pornography, .childAbuse] - } else { - options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] - } + contextController?.dismiss() - presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in - if let reason = reason { - DispatchQueue.main.async { - self?.openChatForReporting(reason) - } - } + self.context.sharedContext.makeContentReportScreen(context: self.context, subject: .peer(self.peerId), forceDark: false, present: { [weak self] controller in + self?.controller?.push(controller) + }, completion: { + }, requestSelectMessages: { [weak self] title, option, message in + self?.openChatForReporting(title: title, option: option, message: message) }) } } @@ -8676,7 +8722,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller else { return } - self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: "", adminRights: nil), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, forceExternal: false, openPeer: { id, navigation in + self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: "", adminRights: nil), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { id, navigation in }, sendFile: nil, sendSticker: nil, @@ -8805,6 +8851,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenRevenue() { + guard let revenueContext = self.data?.revenueStatsContext else { + return + } + let controller = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, section: .monetization, existingRevenueContext: revenueContext, boostStatus: nil) + + self.controller?.push(controller) + } + private func editingOpenStars() { guard let revenueContext = self.data?.starsRevenueStatsContext else { return @@ -10132,35 +10187,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openPremiumGift() { - guard let cachedData = self.data?.cachedData as? CachedUserData else { + guard let premiumGiftOptions = self.data?.premiumGiftOptions else { return } - var pushControllerImpl: ((ViewController) -> Void)? - let controller = PremiumGiftScreen(context: self.context, peerIds: [self.peerId], options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in - pushControllerImpl?(c) - }, completion: { [weak self] in - if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is PeerInfoScreen) && !($0 is PremiumGiftScreen) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: strongSelf.peerId) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break - } - } - if !foundController { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) - } - }) - pushControllerImpl = { [weak controller] c in - controller?.push(c) - } + let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + + let controller = self.context.sharedContext.makeGiftOptionsController( + context: self.context, + peerId: self.peerId, + premiumOptions: premiumOptions + ) self.controller?.push(controller) } @@ -10684,7 +10720,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = anchor { resolvedUrl = .instantView(webPage, customAnchor) } - strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] controller, arguments in self?.controller?.push(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -11990,11 +12026,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } strongSelf.view.endEditing(true) - strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in - self?.controller?.present(c, in: .window(.root), with: a) - }, push: { c in - self?.controller?.push(c) - }, completion: { _, _ in }), in: .window(.root)) + + strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in + self?.controller?.push(controller) + }, completion: {}, requestSelectMessages: nil) }, displayCopyProtectionTip: { [weak self] node, save in if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty { let _ = (strongSelf.context.engine.data.get(EngineDataMap( @@ -12014,7 +12049,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var isBot = false for message in messages { if let author = message.author, case let .user(user) = author { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { isBot = true } break @@ -12024,7 +12059,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if isBot { type = .bot } else if let user = peer as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user @@ -12627,6 +12662,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private weak var requestsContext: PeerInvitationImportersContext? fileprivate let starsContext: StarsContext? private let switchToRecommendedChannels: Bool + private let switchToGifts: Bool private let chatLocation: ChatLocation private let chatLocationContextHolder = Atomic(value: nil) @@ -12683,7 +12719,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId @@ -12697,6 +12733,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.hintGroupInCommon = hintGroupInCommon self.requestsContext = requestsContext self.switchToRecommendedChannels = switchToRecommendedChannels + self.switchToGifts = switchToGifts if let forumTopicThread = forumTopicThread { self.chatLocation = .replyThread(message: forumTopicThread) @@ -13037,7 +13074,13 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: self.switchToRecommendedChannels ? .recommended : nil) + var initialPaneKey: PeerInfoPaneKey? + if self.switchToRecommendedChannels { + initialPaneKey = .recommended + } else if self.switchToGifts { + initialPaneKey = .gifts + } + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) @@ -13066,7 +13109,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.resetHeaderExpansion() } } else { - self.controllerNode.updateNavigation(transition: .immediate, additive: false, animateHeader: false) + self.controllerNode.updateNavigation(transition: .animated(duration: 0.15, curve: .easeInOut), additive: false, animateHeader: false) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 3cf859f6635..1cdc3d2a22a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -98,8 +98,8 @@ final class StorySearchGridScreenComponent: Component { } else { let paneNodeScope: PeerInfoStoryPaneNode.Scope switch component.scope { - case let .query(query): - paneNodeScope = .search(query: query) + case let .query(peer, query): + paneNodeScope = .search(peerId: peer?.id, query: query) case let .location(coordinates, venue): paneNodeScope = .location(coordinates: coordinates, venue: venue) } @@ -273,8 +273,12 @@ public final class StorySearchGridScreen: ViewControllerComponentContainer { title = nil } switch self.scope { - case let .query(query): - self.titleView?.titleContent = .custom("\(query)", title, false) + case let .query(peer, query): + if let peer, let addressName = peer.addressName { + self.titleView?.titleContent = .custom("\(query)@\(addressName)", title, false) + } else { + self.titleView?.titleContent = .custom("\(query)", title, false) + } case .location: self.titleView?.titleContent = .custom(presentationData.strings.StoryGridScreen_TitleLocationSearch, nil, false) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 03dc3895ba4..e0ed6fc28ac 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -48,6 +48,10 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift new file mode 100644 index 00000000000..feed0fde865 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -0,0 +1,433 @@ +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import TelegramUIPreferences +import ItemListPeerItem +import ItemListPeerActionItem +import MergeLists +import ItemListUI +import ChatControllerInteraction +import MultilineTextComponent +import BalancedTextComponent +import Markdown +import PeerInfoPaneNode +import GiftItemComponent +import PlainButtonComponent +import GiftViewScreen +import SolidRoundedButtonNode + +public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { + private let context: AccountContext + private let peerId: PeerId + private let profileGifts: ProfileGiftsContext + + private var dataDisposable: Disposable? + + private let chatControllerInteraction: ChatControllerInteraction + private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void + + public weak var parentController: ViewController? + + private let backgroundNode: ASDisplayNode + private let scrollNode: ASScrollNode + + private var unlockBackground: NavigationBackgroundNode? + private var unlockSeparator: ASDisplayNode? + private var unlockText: ComponentView? + private var unlockButton: SolidRoundedButtonNode? + + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private var theme: PresentationTheme? + private let presentationDataPromise = Promise() + + private let ready = Promise() + private var didSetReady: Bool = false + public var isReady: Signal { + return self.ready.get() + } + + private let statusPromise = Promise(nil) + public var status: Signal { + self.statusPromise.get() + } + + public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var tabBarOffset: CGFloat { + return 0.0 + } + + private var starsProducts: [ProfileGiftsContext.State.StarGift]? + + private var starsItems: [AnyHashable: ComponentView] = [:] + + public init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, profileGifts: ProfileGiftsContext) { + self.context = context + self.peerId = peerId + self.chatControllerInteraction = chatControllerInteraction + self.openPeerContextAction = openPeerContextAction + self.profileGifts = profileGifts + + self.backgroundNode = ASDisplayNode() + self.scrollNode = ASScrollNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.scrollNode) + + self.dataDisposable = (profileGifts.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + let isFirstTime = starsProducts == nil + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts))) + self.starsProducts = state.gifts + + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + + self.updateScrolling(transition: isFirstTime ? .immediate : .easeInOut(duration: 0.25)) + }) + } + + deinit { + self.dataDisposable?.dispose() + } + + public override func didLoad() { + super.didLoad() + + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + self.scrollNode.view.delegate = self + } + + public func ensureMessageIsVisible(id: MessageId) { + } + + public func scrollToTop() -> Bool { + self.scrollNode.view.setContentOffset(.zero, animated: true) + return true + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + func updateScrolling(transition: ComponentTransition) { + if let starsProducts = self.starsProducts, let params = self.currentParams { + let optionSpacing: CGFloat = 10.0 + let sideInset = params.sideInset + 16.0 + + let itemsInRow = min(starsProducts.count, 3) + let optionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) + + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + + let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0) + + let topInset: CGFloat = 60.0 + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: starsOptionSize) + + var index: Int32 = 0 + for product in starsProducts { + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } + + if isVisible { + let itemId = AnyHashable(index) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.starsItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.starsItems[itemId] = visibleItem + itemTransition = .immediate + } + + let ribbonText: String? + if let availability = product.gift.availability { + ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string + } else { + ribbonText = nil + } + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: self.context, + theme: params.presentationData.theme, + peer: product.fromPeer.flatMap { .peer($0) } ?? .anonymous, + subject: .starGift(product.gift.id, product.gift.file), + price: "⭐️ \(product.gift.price)", + ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: .blue) }, + isHidden: !product.savedToProfile + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + let controller = GiftViewScreen( + context: self.context, + subject: .profileGift(self.peerId, product), + updateSavedToProfile: { [weak self] added in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.updateStarGiftAddedToProfile(messageId: messageId, added: added) + }, + convertToStars: { [weak self] in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.convertStarGift(messageId: messageId) + } + ) + self.parentController?.push(controller) + + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: starsOptionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollNode.view.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > params.size.width { + itemFrame.origin.x = sideInset + itemFrame.origin.y += starsOptionSize.height + optionSpacing + } + index += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.starsItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.starsItems.removeValue(forKey: id) + } + + var bottomScrollInset: CGFloat = 0.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 + if self.peerId == self.context.account.peerId { + let transition = ComponentTransition.immediate + + let size = params.size + let sideInset = params.sideInset + let bottomInset = params.bottomInset + let presentationData = params.presentationData + + let themeUpdated = self.theme !== presentationData.theme + self.theme = presentationData.theme + + let unlockText: ComponentView + let unlockBackground: NavigationBackgroundNode + let unlockSeparator: ASDisplayNode + let unlockButton: SolidRoundedButtonNode + if let current = self.unlockText { + unlockText = current + } else { + unlockText = ComponentView() + self.unlockText = unlockText + } + + if let current = self.unlockBackground { + unlockBackground = current + } else { + unlockBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor) + self.addSubnode(unlockBackground) + self.unlockBackground = unlockBackground + } + + if let current = self.unlockSeparator { + unlockSeparator = current + } else { + unlockSeparator = ASDisplayNode() + self.addSubnode(unlockSeparator) + self.unlockSeparator = unlockSeparator + } + + if let current = self.unlockButton { + unlockButton = current + } else { + unlockButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0) + self.view.addSubview(unlockButton.view) + self.unlockButton = unlockButton + + unlockButton.title = params.presentationData.strings.PeerInfo_Gifts_Send + + unlockButton.pressed = { [weak self] in + self?.buttonPressed() + } + } + + if themeUpdated { + unlockBackground.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) + unlockSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor + unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) + } + + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) + let textColor = presentationData.theme.list.itemSecondaryTextColor + let linkColor = presentationData.theme.list.itemAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in + return nil + }) + + let scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight) + + let buttonSideInset = sideInset + 16.0 + let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) + transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) + let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) + + let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 + transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight)) + unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) + transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) + + let unlockSize = unlockText.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .markdown(text: params.presentationData.strings.PeerInfo_Gifts_Info, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 200.0) + ) + if let view = unlockText.view { + if view.superview == nil { + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonPressed))) + self.scrollNode.view.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize)) + } + contentHeight += unlockSize.height + contentHeight += bottomPanelHeight + + bottomScrollInset = bottomPanelHeight - 40.0 + } + contentHeight += params.bottomInset + + self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 50.0, left: 0.0, bottom: bottomScrollInset, right: 0.0) + + let contentSize = CGSize(width: params.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != contentSize { + self.scrollNode.view.contentSize = contentSize + } + } + + let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) + if bottomContentOffset < 200.0 { + self.profileGifts.loadMore() + } + } + + @objc private func buttonPressed() { + let _ = (self.context.account.stateManager.contactBirthdays + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] birthdays in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.chatControllerInteraction.navigationController()?.pushViewController(controller) + }) + } + + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) + self.presentationDataPromise.set(.single(presentationData)) + + self.backgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: size)) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + if isScrollingLockedAtTop { + self.scrollNode.view.contentOffset = .zero + } + self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop + + self.updateScrolling(transition: ComponentTransition(transition)) + } + + public func findLoadedMessage(id: MessageId) -> Message? { + return nil + } + + public func updateHiddenMedia() { + } + + public func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { +// self.scrollNode.transferVelocity(velocity) + } + } + + public func cancelPreviewGestures() { + } + + public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + public func addToTransitionSurface(view: UIView) { + } + + public func updateSelectedMessages(animated: Bool) { + } +} + +private struct StarsGiftProduct: Equatable { + let emoji: String + let price: Int64 + let isLimited: Bool +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 3eaa03290d4..f3c14f0fba3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1492,7 +1492,7 @@ private final class StorySearchHeaderComponent: Component { public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum Scope { case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) - case search(query: String) + case search(peerId: EnginePeer.Id?, query: String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) case botPreview(id: EnginePeer.Id) } @@ -1749,8 +1749,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr switch self.scope { case let .peer(id, _, isArchived): self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived) - case let .search(query): - self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(query)) + case let .search(peerId, query): + self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query)) case let .location(coordinates, venue): self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) case let .botPreview(id): @@ -1770,7 +1770,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak self] count in + |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { return } @@ -2420,7 +2420,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.updateDisposable.dispose() self.mapDisposable?.dispose() } - + public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { let listSource = self.listSource return Signal { subscriber in @@ -2862,6 +2862,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr continue } + var authorPeer = item.peer var isReorderable = false switch self.scope { case .botPreview: @@ -2870,16 +2871,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if id == self.context.account.peerId { isReorderable = state.pinnedIds.contains(item.storyItem.id) } + case let .search(peerId, _): + if peerId != nil { + authorPeer = nil + } default: break } - + mappedItems.append(VisualMediaItem( index: mappedItems.count, peer: peerReference, storyId: item.id, story: item.storyItem, - authorPeer: item.peer, + authorPeer: authorPeer, isPinned: state.pinnedIds.contains(item.storyItem.id), localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, isReorderable: isReorderable @@ -4104,7 +4109,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.isProfileEmbedded, case .botPreview = self.scope { self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { - self.view.backgroundColor = .clear + if case let .search(peerId, _) = self.scope, peerId != nil { + + } else { + self.view.backgroundColor = .clear + } } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index f43cb26bcaf..4ccb715b37a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -709,7 +709,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, - attributes: [.FileName(fileName: "file")] + attributes: [.FileName(fileName: "file")], + alternativeRepresentations: [] ) let fakeMessage = Message( stableId: 1, @@ -870,7 +871,10 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } let message = item.message - let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id) + var hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id) + if message.isSensitiveContent(platform: "ios") { + hasSpoiler = true + } layer.updateHasSpoiler(hasSpoiler: hasSpoiler) var selectedMedia: Media? @@ -1272,7 +1276,11 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, } strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue) } else { - let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default)) + if item.message.isSensitiveContent(platform: "ios") { +// strongSelf.context.currentContentSettings.with { $0 }.ignoreContentRestrictionReasons + } else { + let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default)) + } } } @@ -2126,7 +2134,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, - attributes: [.FileName(fileName: "file")] + attributes: [.FileName(fileName: "file")], + alternativeRepresentations: [] ) let fakeMessage = Message( stableId: 1, diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD index b4afc963ea8..86009f900a0 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD index 86c4687698e..78646cc8ce3 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/BUILD b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/BUILD index 5725286c77f..f17e11f5b1a 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/BUILD +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD index 1ca248fb024..567de6bcad6 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD @@ -17,7 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/AttachmentUI", - "//submodules/PremiumUI", + "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift index 520c5d79da6..f9ab8d7db74 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -5,15 +5,14 @@ import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import AccountContext -import PremiumUI import AttachmentUI +import GiftOptionsScreen -public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable { +public class PremiumGiftAttachmentScreen: GiftOptionsScreen, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } - public var parentController: () -> ViewController? = { - return nil - } + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -25,17 +24,16 @@ public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainab } private final class PremiumGiftContext: AttachmentMediaPickerContext { - private weak var controller: PremiumGiftScreen? + private weak var controller: GiftOptionsScreen? public var mainButtonState: Signal { - return self.controller?.mainButtonStatePromise.get() ?? .single(nil) + return .single(nil) } - init(controller: PremiumGiftScreen) { + init(controller: GiftOptionsScreen) { self.controller = controller } func mainButtonAction() { - self.controller?.mainButtonPressed() } } diff --git a/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD b/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD new file mode 100644 index 00000000000..a18681c9a5c --- /dev/null +++ b/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "RasterizedCompositionComponent", + module_name = "RasterizedCompositionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift b/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift new file mode 100644 index 00000000000..cc45809ad1d --- /dev/null +++ b/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift @@ -0,0 +1,388 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import UIKitRuntimeUtils +import ComponentFlow + +open class RasterizedCompositionLayer: CALayer { + private final class SublayerReference { + weak var layer: CALayer? + + init(layer: CALayer) { + self.layer = layer + } + } + + private var sublayerReferences: [SublayerReference] = [] + + public var onUpdatedIsAnimating: (() -> Void)? + public var onContentsUpdated: (() -> Void)? + + override public var position: CGPoint { + didSet { + if self.position != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var bounds: CGRect { + didSet { + if self.bounds != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var transform: CATransform3D { + didSet { + if !CATransform3DEqualToTransform(self.transform, oldValue) { + self.onContentsUpdated?() + } + } + } + + override public var opacity: Float { + didSet { + if self.opacity != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var isHidden: Bool { + didSet { + if self.isHidden != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var backgroundColor: CGColor? { + didSet { + if let lhs = self.backgroundColor, let rhs = oldValue { + if lhs != rhs { + self.onContentsUpdated?() + } + } else if (self.backgroundColor == nil) != (oldValue == nil) { + self.onContentsUpdated?() + } + } + } + + override public var cornerRadius: CGFloat { + didSet { + if self.cornerRadius != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var masksToBounds: Bool { + didSet { + if self.masksToBounds != oldValue { + self.onContentsUpdated?() + } + } + } + + public var hasAnimationsInTree: Bool { + if let animationKeys = self.animationKeys(), !animationKeys.isEmpty { + return true + } + if let sublayers = self.sublayers { + for sublayer in sublayers { + if let sublayer = sublayer as? RasterizedCompositionLayer { + if sublayer.hasAnimationsInTree { + return true + } + } + } + } + return false + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func onLayerAdded(layer: CALayer) { + if !self.sublayerReferences.contains(where: { $0.layer === layer }) { + self.sublayerReferences.append(SublayerReference(layer: layer)) + } + if let layer = layer as? RasterizedCompositionLayer { + layer.onUpdatedIsAnimating = { [weak self] in + self?.onUpdatedIsAnimating?() + } + layer.onContentsUpdated = { [weak self] in + self?.onContentsUpdated?() + } + } else { + assertionFailure() + } + + self.onUpdatedIsAnimating?() + self.onContentsUpdated?() + } + + private func cleanupSublayerReferences() { + for i in (0 ..< self.sublayerReferences.count).reversed() { + if let layer = sublayerReferences[i].layer { + if layer.superlayer !== self { + sublayerReferences.remove(at: i) + } + } else { + sublayerReferences.remove(at: i) + } + } + } + + override public func addSublayer(_ layer: CALayer) { + super.addSublayer(layer) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, at idx: UInt32) { + super.insertSublayer(layer, at: idx) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, below sibling: CALayer?) { + super.insertSublayer(layer, below: sibling) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, above sibling: CALayer?) { + super.insertSublayer(layer, above: sibling) + + self.onLayerAdded(layer: layer) + } + + override public func replaceSublayer(_ oldLayer: CALayer, with newLayer: CALayer) { + super.replaceSublayer(oldLayer, with: newLayer) + + self.onLayerAdded(layer: newLayer) + } + + override public func add(_ anim: CAAnimation, forKey key: String?) { + let anim = anim.copy() as! CAAnimation + let completion = anim.completion + anim.completion = { [weak self] flag in + completion?(flag) + + guard let self else { + return + } + self.onUpdatedIsAnimating?() + } + + super.add(anim, forKey: key) + } + + override public func removeAllAnimations() { + super.removeAllAnimations() + + self.onUpdatedIsAnimating?() + } + + override public func removeAnimation(forKey key: String) { + super.removeAnimation(forKey: key) + + if let animationKeys = self.animationKeys(), !animationKeys.isEmpty { + } else { + self.onUpdatedIsAnimating?() + } + } +} + +public final class RasterizedCompositionImageLayer: RasterizedCompositionLayer { + public var image: UIImage? { + didSet { + if self.image !== oldValue { + if let image = self.image { + let capInsets = image.capInsets + if capInsets.left.isZero && capInsets.top.isZero && capInsets.right.isZero && capInsets.bottom.isZero { + self.contentsScale = image.scale + self.contents = image.cgImage + } else { + ASDisplayNodeSetResizableContents(self, image) + } + } else { + self.contents = nil + } + self.onContentsUpdated?() + } + } + } +} + +private func calculateSublayerBounds(layer: CALayer) -> CGRect { + var result: CGRect + if layer.contents != nil { + result = layer.bounds + } else { + result = CGRect() + } + + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let sublayerBounds = sublayer.convert(sublayer.bounds, to: layer) + if result.isEmpty { + result = sublayerBounds + } else { + result = result.union(sublayerBounds) + } + } + } + + return result +} + +public final class RasterizedCompositionMonochromeLayer: SimpleLayer { + public let contentsLayer = RasterizedCompositionLayer() + public let maskedLayer = SimpleLayer() + public let rasterizedLayer = SimpleLayer() + + private var isContentsUpdateScheduled: Bool = false + private var isRasterizationModeUpdateScheduled: Bool = false + + override public init() { + super.init() + + self.maskedLayer.opacity = 0.0 + self.addSublayer(self.maskedLayer) + + self.maskedLayer.mask = self.contentsLayer + self.maskedLayer.rasterizationScale = UIScreenScale + + self.contentsLayer.backgroundColor = UIColor.black.cgColor + if let filter = makeLuminanceToAlphaFilter() { + self.contentsLayer.filters = [filter] + } + self.contentsLayer.rasterizationScale = UIScreenScale + + self.addSublayer(self.rasterizedLayer) + + self.contentsLayer.onContentsUpdated = { [weak self] in + guard let self else { + return + } + if !self.contentsLayer.hasAnimationsInTree { + self.scheduleContentsUpdate() + } + } + + self.contentsLayer.onUpdatedIsAnimating = { [weak self] in + guard let self else { + return + } + self.scheduleUpdateRasterizationMode() + } + + self.isContentsUpdateScheduled = true + self.isRasterizationModeUpdateScheduled = true + self.setNeedsLayout() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + private func scheduleContentsUpdate() { + self.isContentsUpdateScheduled = true + self.setNeedsLayout() + } + + private func scheduleUpdateRasterizationMode() { + self.isRasterizationModeUpdateScheduled = true + self.setNeedsLayout() + } + + override public func layoutSublayers() { + super.layoutSublayers() + + if self.isRasterizationModeUpdateScheduled { + self.isRasterizationModeUpdateScheduled = false + self.updateRasterizationMode() + } + if self.isContentsUpdateScheduled { + self.isContentsUpdateScheduled = false + if !self.contentsLayer.hasAnimationsInTree { + self.updateContents() + } + } + } + + private func updateContents() { + var contentBounds = calculateSublayerBounds(layer: self.contentsLayer) + contentBounds.size.width = ceil(contentBounds.width) + contentBounds.size.height = ceil(contentBounds.height) + self.rasterizedLayer.frame = contentBounds + let contentsImage = generateImage(contentBounds.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: -contentBounds.minX, y: -contentBounds.minY) + + self.contentsLayer.render(in: context) + }) + + if let contentsImage { + if let context = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true), let alphaContext = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true) { + context.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + c.clear(CGRect(origin: CGPoint(), size: context.size)) + contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0) + } + alphaContext.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + c.clear(CGRect(origin: CGPoint(), size: context.size)) + contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0) + } + context.blt(alphaContext, at: CGPoint(), mode: .AlphaFromColor) + + self.rasterizedLayer.contents = context.generateImage()?.cgImage + } + } else { + self.rasterizedLayer.contents = nil + } + } + + private func updateRasterizationMode() { + self.maskedLayer.opacity = self.contentsLayer.hasAnimationsInTree ? 1.0 : 0.0 + if self.rasterizedLayer.isHidden != (self.maskedLayer.opacity != 0.0) { + self.rasterizedLayer.isHidden = self.maskedLayer.opacity != 0.0 + if !self.rasterizedLayer.isHidden { + self.updateContents() + } + } + } +} diff --git a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD index e9de562525e..0c619ea28f1 100644 --- a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Display", diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD new file mode 100644 index 00000000000..aa4cbd29ef4 --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SectionTitleContextItem", + module_name = "SectionTitleContextItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ContextUI", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift new file mode 100644 index 00000000000..03cb0e9a238 --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift @@ -0,0 +1,89 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData + +public final class SectionTitleContextItem: ContextMenuCustomItem { + let text: String + + public init(text: String) { + self.text = text + } + + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return SectionTitleContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class SectionTitleContextItemNode: ASDisplayNode, ContextMenuCustomNode { + private let item: SectionTitleContextItem + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + var needsSeparator: Bool { + return false + } + + init(presentationData: PresentationData, item: SectionTitleContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + self.textNode.maximumNumberOfLines = 1 + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 16.0 + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - sideInset, height: .greatestFiniteMagnitude)) + let height: CGFloat = 28.0 + + return (CGSize(width: textSize.width + sideInset + sideInset, height: height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } +} diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift index bcc480988f6..fb4c0c5aa38 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift @@ -192,6 +192,7 @@ public final class ArchiveInfoContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift index 0bee05ace12..5bc9281d030 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift @@ -108,11 +108,9 @@ private final class ArchiveInfoSheetContentComponent: Component { } contentHeight += buttonSize.height - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 14.0 - } + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + contentHeight += bottomInset return CGSize(width: availableSize.width, height: contentHeight) } @@ -226,7 +224,7 @@ private final class ArchiveInfoScreenComponent: Component { }) } )), - backgroundColor: .color(environment.theme.list.plainBackgroundColor), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 6aeef906f75..a038c5413b0 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -186,7 +186,7 @@ final class GreetingMessageListItemComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index bf1e17537d7..1475fb6bbb6 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -201,7 +201,7 @@ final class QuickReplySetupScreenComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift index 16ed898dd4c..b1fdea8c21a 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift @@ -189,6 +189,7 @@ public final class BirthdayPickerContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: mainText.string.contains(">") ? UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0) : .zero, highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 20e342ee4d8..203a6b76b16 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -616,6 +616,7 @@ final class ChatbotSetupScreenComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.25, highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift index de5d8b52b90..1d385f09862 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -108,7 +108,7 @@ private final class PeerBadgeComponent: Component { let _ = self.background.update( transition: transition, - component: AnyComponent(RoundedRectangle(color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: nil)), + component: AnyComponent(RoundedRectangle(color: component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), cornerRadius: nil)), environment: {}, containerSize: size ) @@ -246,25 +246,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: username.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(username.info.cryptoCurrencyAmount, currency: username.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(username.info.currencyAmount, currency: username.info.currency) + let cryptoCurrencyText = formatTonAmountText(username.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(username.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { @@ -292,25 +277,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: phoneNumber.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.cryptoCurrencyAmount, currency: phoneNumber.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.currencyAmount, currency: phoneNumber.info.currency) + let cryptoCurrencyText = formatTonAmountText(phoneNumber.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(phoneNumber.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD index 802cc7a542b..ec78a408405 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Display", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 2ca28c9923d..8554d99355b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -1310,7 +1310,7 @@ final class ChannelAppearanceScreenComponent: Component { var emojiPackFile: TelegramMediaFile? if let thumbnail = emojiPack?.thumbnail { - emojiPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) + emojiPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []) } let emojiPackSectionSize = self.emojiPackSection.update( @@ -1438,7 +1438,7 @@ final class ChannelAppearanceScreenComponent: Component { var stickerPackFile: TelegramMediaFile? if let peerStickerPack = contentsData.peerStickerPack, let thumbnail = peerStickerPack.thumbnail { - stickerPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: peerStickerPack.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) + stickerPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: peerStickerPack.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []) } let stickerPackSectionSize = self.stickerPackSection.update( diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 819d491cbdc..2955564155b 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { @@ -1072,7 +1072,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) diff --git a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift index a96132f7155..bd7a4fef680 100644 --- a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift +++ b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift @@ -8,12 +8,14 @@ import TelegramPresentationData import AnimatedCountLabelNode public final class SliderContextItem: ContextMenuCustomItem { + private let title: String? private let minValue: CGFloat private let maxValue: CGFloat private let value: CGFloat private let valueChanged: (CGFloat, Bool) -> Void - public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + public init(title: String? = nil, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value @@ -21,7 +23,7 @@ public final class SliderContextItem: ContextMenuCustomItem { } public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { - return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) + return SliderContextItemNode(presentationData: presentationData, getController: getController, title: self.title, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) } } @@ -31,12 +33,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private var presentationData: PresentationData private(set) var vibrancyEffectView: UIVisualEffectView? + + private let backgroundTitleNode: ImmediateTextNode + private let dimBackgroundTitleNode: ImmediateTextNode + private let foregroundTitleNode: ImmediateTextNode + private let backgroundTextNode: ImmediateAnimatedCountLabelNode private let dimBackgroundTextNode: ImmediateAnimatedCountLabelNode private let foregroundNode: ASDisplayNode private let foregroundTextNode: ImmediateAnimatedCountLabelNode + let title: String? let minValue: CGFloat let maxValue: CGFloat var value: CGFloat = 1.0 { @@ -49,13 +57,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private let hapticFeedback = HapticFeedback() - init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, title: String?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.presentationData = presentationData + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value self.valueChanged = valueChanged + self.backgroundTitleNode = ImmediateTextNode() + self.dimBackgroundTitleNode = ImmediateTextNode() + self.foregroundTitleNode = ImmediateTextNode() + self.backgroundTextNode = ImmediateAnimatedCountLabelNode() self.backgroundTextNode.alwaysOneDirection = true @@ -76,7 +89,6 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.isUserInteractionEnabled = true if presentationData.theme.overallDarkAppearance { - } else { let style: UIBlurEffect.Style style = .extraLight @@ -87,9 +99,12 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.vibrancyEffectView = vibrancyEffectView } + self.addSubnode(self.backgroundTitleNode) + self.addSubnode(self.dimBackgroundTitleNode) self.addSubnode(self.backgroundTextNode) self.addSubnode(self.dimBackgroundTextNode) self.addSubnode(self.foregroundNode) + self.foregroundNode.addSubnode(self.foregroundTitleNode) self.foregroundNode.addSubnode(self.foregroundTextNode) let stringValue = "1.0x" @@ -114,6 +129,11 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, textCount += 1 } } + + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + self.dimBackgroundTextNode.segments = dimBackgroundSegments self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments @@ -179,6 +199,10 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + let _ = self.dimBackgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.foregroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) @@ -188,20 +212,41 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, let valueWidth: CGFloat = 70.0 let height: CGFloat = 45.0 - var backgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + let originalBackgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + var backgroundTextSize = originalBackgroundTextSize backgroundTextSize.width = valueWidth + let backgroundTitleSize = self.backgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.dimBackgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.foregroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + return (CGSize(width: height * 3.0, height: height), { size, transition in let leftInset: CGFloat = 17.0 self.vibrancyEffectView?.frame = CGRect(origin: .zero, size: size) - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + let backgroundTextWidth = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true).width + + self.updateValue(transition: transition) + + let titleFrame: CGRect + let textFrame: CGRect + + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTitleSize.height) / 2.0)), size: backgroundTitleSize) + + if self.title != nil { + textFrame = CGRect(origin: CGPoint(x: size.width - leftInset - backgroundTextWidth, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } else { + textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } + + transition.updateFrameAdditive(node: self.dimBackgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.backgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.foregroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.dimBackgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame) - - self.updateValue(transition: transition) }) } diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD index 2118beea582..61978160eda 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/BUILD +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/Display", diff --git a/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD new file mode 100644 index 00000000000..2f14515a874 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ItemShimmeringLoadingComponent", + module_name = "ItemShimmeringLoadingComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/TextLoadingEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift similarity index 69% rename from submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift rename to submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift index 96600232dd6..83377ab8e1c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift +++ b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift @@ -6,17 +6,25 @@ import HierarchyTrackingLayer import ComponentFlow import TextLoadingEffect -final class ItemLoadingComponent: Component { +public final class ItemShimmeringLoadingComponent: Component { private let color: UIColor + private let cornerRadius: CGFloat - public init(color: UIColor) { + public init( + color: UIColor, + cornerRadius: CGFloat = 10.0 + ) { self.color = color + self.cornerRadius = cornerRadius } - public static func ==(lhs: ItemLoadingComponent, rhs: ItemLoadingComponent) -> Bool { + public static func ==(lhs: ItemShimmeringLoadingComponent, rhs: ItemShimmeringLoadingComponent) -> Bool { if !lhs.color.isEqual(rhs.color) { return false } + if lhs.cornerRadius != rhs.cornerRadius { + return false + } return true } @@ -28,16 +36,14 @@ final class ItemLoadingComponent: Component { private let borderMaskGradientView = UIImageView() private let borderMaskFillView = UIImageView() - private var component: ItemLoadingComponent? + private var component: ItemShimmeringLoadingComponent? override public init(frame: CGRect) { super.init(frame: frame) self.addSubview(self.loadingView) self.addSubview(self.borderView) - - self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: 10.0, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10).withRenderingMode(.alwaysTemplate) - + self.borderMaskView.backgroundColor = .clear self.borderMaskFillView.backgroundColor = .white @@ -63,14 +69,23 @@ final class ItemLoadingComponent: Component { }) } - func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: ItemShimmeringLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil + let previousCornerRadius = self.component?.cornerRadius self.component = component + if previousCornerRadius != component.cornerRadius { + self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: component.cornerRadius, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)).withRenderingMode(.alwaysTemplate) + } + self.borderView.tintColor = component.color self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize)) + self.loadingView.frame = CGRect(origin: .zero, size: availableSize) + self.loadingView.layer.cornerRadius = component.cornerRadius + self.loadingView.clipsToBounds = true + transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize)) self.borderMaskView.frame = self.borderView.bounds diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD index 4eabbf93fc2..6c1281d69f6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index e05bba66bdc..5a012346371 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -274,6 +274,19 @@ public final class StarsAvatarComponent: Component { self.iconView.isHidden = false self.avatarNode.isHidden = true self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + case .apiLimitExtension: + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0x32b83b).cgColor, + UIColor(rgb: 0x87d93b).cgColor + ], + direction: .vertical + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/PaidBroadcast") case .unsupported: iconInset = 7.0 self.backgroundView.image = generateGradientFilledCircleImage( diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD index b71d4ed3c09..4f6a2a6b5ca 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 09afb0a4e34..fc2a18c3aa6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -788,6 +788,16 @@ public final class StarsImageComponent: Component { direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + case .apiLimitExtension: + iconBackgroundView.image = generateGradientFilledCircleImage( + diameter: imageSize.width, + colors: [ + UIColor(rgb: 0x32b83b).cgColor, + UIColor(rgb: 0x87d93b).cgColor + ], + direction: .vertical + ) + iconView.image = UIImage(bundleImageName: "Premium/Stars/PaidBroadcast") case .peer, .unsupported: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD new file mode 100644 index 00000000000..49e70031d52 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD @@ -0,0 +1,42 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StarsIntroScreen", + module_name = "StarsIntroScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/BlurredBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift new file mode 100644 index 00000000000..3bcf7b4b6bc --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -0,0 +1,534 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import ButtonComponent +import AccountContext +import SheetComponent +import BlurredBackgroundComponent +import PremiumStarComponent + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let openExamples: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + openExamples: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.openExamples = openExamples + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let star = Child(PremiumStarComponent.self) + let title = Child(BalancedTextComponent.self) + let text = Child(BalancedTextComponent.self) + let list = Child(List.self) + let actionButton = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.semibold(20.0) + let textFont = Font.regular(15.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 152.0) + + let star = star.update( + component: PremiumStarComponent( + theme: environment.theme, + isIntro: true, + isVisible: true, + hasIdleAnimations: true, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ], + particleColor: UIColor(rgb: 0xf9b004), + backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: 79.0)) + ) + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: strings.Stars_Info_Title, font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing - 8.0 + + let text = text.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: strings.Stars_Info_Description, font: textFont, textColor: secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += spacing + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "gift", + component: AnyComponent(ParagraphComponent( + title: strings.Stars_Info_Gift_Title, + titleColor: textColor, + text: strings.Stars_Info_Gift_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Gift", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "miniapp", + component: AnyComponent(ParagraphComponent( + title: strings.Stars_Info_Miniapp_Title, + titleColor: textColor, + text: strings.Stars_Info_Miniapp_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Miniapp", + iconColor: linkColor, + action: { + component.openExamples() + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "media", + component: AnyComponent(ParagraphComponent( + title: strings.Stars_Info_Media_Title, + titleColor: textColor, + text: strings.Stars_Info_Media_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Media", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "reaction", + component: AnyComponent(ParagraphComponent( + title: strings.Stars_Info_Reaction_Title, + titleColor: textColor, + text: strings.Stars_Info_Reaction_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Reaction", + iconColor: linkColor + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing + + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + + contentSize.height += bottomPanelPadding + + let controller = environment.controller() as? StarsIntroScreen + let actionButton = actionButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: strings.Stars_Info_Done, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { + controller?.dismissAnimated() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: buttonHeight), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + .cornerRadius(10.0) + ) + contentSize.height += actionButton.size.height + bottomInset + + return contentSize + } + } +} + +private final class ContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let openExamples: () -> Void + + init( + context: AccountContext, + openExamples: @escaping () -> Void + ) { + self.context = context + self.openExamples = openExamples + } + + static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var topContentOffset: CGFloat? + var bottomContentOffset: CGFloat? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + openExamples: context.component.openExamples, + dismiss: { + controller()?.dismiss() + } + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class StarsIntroScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + forceDark: Bool = false + ) { + self.context = context + + var openExamplesImpl: (() -> Void)? + super.init( + context: context, + component: ContainerComponent( + context: context, + openExamples: { + openExamplesImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + openExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let iconName: String + let iconColor: UIColor + let action: () -> Void + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + iconName: String, + iconColor: UIColor, + action: @escaping () -> Void = {} + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, UIColor)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + let state = context.state + + let leftInset: CGFloat = 32.0 + let rightInset: CGFloat = 24.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let accentColor = component.accentColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 != accentColor { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, accentColor) + } + let textAttributedString = parseMarkdownIntoAttributedString(component.text, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = textAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + textAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: textAttributedString.string)) + } + + let text = text.update( + component: MultilineTextComponent( + text: .plain(textAttributedString), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.action() + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD index dd5b8cbbb61..218ad433e88 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", @@ -37,6 +37,9 @@ swift_library( "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BundleIconComponent", "//submodules/ConfettiEffect", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 54935104050..b341040fc97 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -25,6 +25,7 @@ import TextFormat import PremiumStarComponent import BundleIconComponent import ConfettiEffect +import ItemShimmeringLoadingComponent private struct StarsProduct: Equatable { enum Option: Equatable { @@ -236,6 +237,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .unlockMedia: textString = strings.Stars_Purchase_StarsNeededUnlockInfo + case .starGift: + textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in @@ -259,7 +262,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -328,7 +332,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let backgroundComponent: AnyComponent? if product.storeProduct.id == context.component.selectedProductId { backgroundComponent = AnyComponent( - ItemLoadingComponent(color: environment.theme.list.itemAccentColor) + ItemShimmeringLoadingComponent(color: environment.theme.list.itemAccentColor) ) } else { backgroundComponent = nil @@ -814,11 +818,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { switch context.component.purpose { case .generic: titleText = strings.Stars_Purchase_GetStars - case let .topUp(requiredStars, _): - titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) case .gift: titleText = strings.Stars_Purchase_GiftStars - case let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): + case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -1238,6 +1240,8 @@ private extension StarsPurchasePurpose { return [peerId] case let .subscription(peerId, _, _): return [peerId] + case let .starGift(peerId, _): + return [peerId] default: return [] } @@ -1255,6 +1259,8 @@ private extension StarsPurchasePurpose { return requiredStars case let .unlockMedia(requiredStars): return requiredStars + case let .starGift(_, requiredStars): + return requiredStars default: return nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index b814269b7d8..d20729cb5c5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", @@ -36,6 +36,7 @@ swift_library( "//submodules/GalleryUI", "//submodules/TelegramUI/Components/MiniAppListScreen", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 4e8ba3c5663..ba8cdc83973 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -25,6 +25,7 @@ import GalleryUI import StarsAvatarComponent import MiniAppListScreen import PremiumStarComponent +import GiftAnimationComponent private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -145,6 +146,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let title = Child(MultilineTextComponent.self) let star = Child(StarsImageComponent.self) let activeStar = Child(PremiumStarComponent.self) + let gift = Child(GiftAnimationComponent.self) let amountBackground = Child(RoundedRectangle.self) let amount = Child(BalancedTextComponent.self) let amountStar = Child(BundleIconComponent.self) @@ -225,6 +227,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isReaction = false var giveawayMessageId: MessageId? var isBoost = false + var giftAnimation: TelegramMediaFile? var delayedCloseOnOpenPeer = true switch subject { @@ -322,7 +325,18 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } case let .transaction(transaction, parentPeer): - if let giveawayMessageIdValue = transaction.giveawayMessageId { + if let starGift = transaction.starGift { + titleText = strings.Stars_Transaction_Gift_Title + descriptionText = "" + count = transaction.count + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } + transactionPeer = transaction.peer + giftAnimation = starGift.file + } else if let giveawayMessageIdValue = transaction.giveawayMessageId { titleText = strings.Stars_Transaction_Giveaway_Title descriptionText = "" count = transaction.count @@ -396,10 +410,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { case .ads: titleText = strings.Stars_Transaction_TelegramAds_Title via = strings.Stars_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + titleText = strings.Stars_Transaction_TelegramBotApi_Title case .unsupported: titleText = strings.Stars_Transaction_Unsupported_Title } - if !transaction.media.isEmpty { + + if let floodskipNumber = transaction.floodskipNumber { + descriptionText = strings.Stars_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else if !transaction.media.isEmpty { var description: String = "" var photoCount: Int32 = 0 var videoCount: Int32 = 0 @@ -572,7 +591,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageIcon = nil } var starChild: _UpdatedChildComponent - if isBoost { + if let giftAnimation { + starChild = gift.update( + component: GiftAnimationComponent( + context: component.context, + theme: theme, + file: giftAnimation + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + } else if isBoost { starChild = activeStar.update( component: PremiumStarComponent( theme: theme, @@ -877,7 +906,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) context.add(starChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: starChild.size.height / 2.0 - 19.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 200.0 / 2.0 - 19.0)) ) context.add(title @@ -885,7 +914,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) var originY: CGFloat = 0.0 - originY += starChild.size.height - 23.0 + originY += 200.0 - 23.0 var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { @@ -909,7 +938,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 5, lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index 2647ba93b58..063d341b836 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", @@ -47,6 +47,8 @@ swift_library( "//submodules/StatisticsUI", "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index ee6ff8b47ff..6a883c8df1f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -28,7 +28,6 @@ final class StarsStatisticsScreenComponent: Component { let context: AccountContext let peerId: EnginePeer.Id let revenueContext: StarsRevenueStatsContext - let transactionsContext: StarsTransactionsContext let openTransaction: (StarsContext.State.Transaction) -> Void let withdraw: () -> Void let showTimeoutTooltip: (Int32) -> Void @@ -38,7 +37,6 @@ final class StarsStatisticsScreenComponent: Component { context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext, - transactionsContext: StarsTransactionsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, withdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, @@ -47,7 +45,6 @@ final class StarsStatisticsScreenComponent: Component { self.context = context self.peerId = peerId self.revenueContext = revenueContext - self.transactionsContext = transactionsContext self.openTransaction = openTransaction self.withdraw = withdraw self.showTimeoutTooltip = showTimeoutTooltip @@ -71,6 +68,21 @@ final class StarsStatisticsScreenComponent: Component { override func touchesShouldCancel(in view: UIView) -> Bool { return true } + + override var contentOffset: CGPoint { + set(value) { + var value = value + if value.y > self.contentSize.height - self.bounds.height { + value.y = max(0.0, self.contentSize.height - self.bounds.height) + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { @@ -92,7 +104,11 @@ final class StarsStatisticsScreenComponent: Component { } if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl { - return !view.isTracking + if view is UIButton { + return true + } else { + return !view.isTracking + } } return true @@ -124,7 +140,12 @@ final class StarsStatisticsScreenComponent: Component { private let transactionsHeader = ComponentView() private let transactionsBackground = UIView() - private let transactionsView = ComponentView() + + private let panelContainer = ComponentView() + + private var allTransactionsContext: StarsTransactionsContext? + private var incomingTransactionsContext: StarsTransactionsContext? + private var outgoingTransactionsContext: StarsTransactionsContext? private var component: StarsStatisticsScreenComponent? private weak var state: EmptyComponentState? @@ -132,6 +153,12 @@ final class StarsStatisticsScreenComponent: Component { private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? private var controller: (() -> ViewController?)? + private var enableVelocityTracking: Bool = false + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + + private var listIsExpanded = false + private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? @@ -194,10 +221,36 @@ final class StarsStatisticsScreenComponent: Component { self.stateDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollToTop() { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.enableVelocityTracking = true + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -205,57 +258,128 @@ final class StarsStatisticsScreenComponent: Component { if let view = self.chartView.view as? ListItemComponentAdaptor.View, let node = view.itemNode as? StatsGraphItemNode { node.resetInteraction() } + + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + panelContainerView.transferVelocity(self.previousVelocityM1) + } + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let _ = self.navigationMetrics else { + return + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.enableVelocityTracking = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 } } private var lastScrollBounds: CGRect? private var lastBottomOffset: CGFloat? private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { - return - } - let scrollBounds = self.scrollView.bounds - - let topContentOffset = self.scrollView.contentOffset.y - let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 - - let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) - animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) - animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) - - let expansionDistance: CGFloat = 32.0 - var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance - expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) - - transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) - - let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) - self.lastBottomOffset = bottomOffset - - let transactionsScrollBounds: CGRect - if let transactionsView = self.transactionsView.view { - transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) - } else { - transactionsScrollBounds = .zero + + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height + + if let _ = self.navigationMetrics { + let topContentOffset = self.scrollView.contentOffset.y + let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 + + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) + } + + let listIsExpanded = expansionDistanceFactor == 0.0 + if listIsExpanded != self.listIsExpanded { + self.listIsExpanded = listIsExpanded + if !self.isUpdating { + self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) + } + } } - self.lastScrollBounds = transactionsScrollBounds - let _ = self.transactionsView.updateEnvironment( + let _ = self.panelContainer.updateEnvironment( transition: transition, environment: { - StarsTransactionsPanelEnvironment( - theme: environment.theme, - strings: environment.strings, - dateTimeFormat: environment.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), - isScrollable: false, - isCurrent: true, - externalScrollBounds: transactionsScrollBounds, - externalBottomOffset: bottomOffset - ) + StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels) } ) +// guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { +// return +// } +// +// let scrollBounds = self.scrollView.bounds +// +// let topContentOffset = self.scrollView.contentOffset.y +// let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 +// +// let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) +// animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) +// animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) +// +// let expansionDistance: CGFloat = 32.0 +// var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance +// expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) +// +// transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) +// +// let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) +// self.lastBottomOffset = bottomOffset +// +// let transactionsScrollBounds: CGRect +// if let transactionsView = self.transactionsView.view { +// transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) +// } else { +// transactionsScrollBounds = .zero +// } +// self.lastScrollBounds = transactionsScrollBounds +// +// let _ = self.transactionsView.updateEnvironment( +// transition: transition, +// environment: { +// StarsTransactionsPanelEnvironment( +// theme: environment.theme, +// strings: environment.strings, +// dateTimeFormat: environment.dateTimeFormat, +// containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), +// isScrollable: false, +// isCurrent: true, +// externalScrollBounds: transactionsScrollBounds, +// externalBottomOffset: bottomOffset +// ) +// } +// ) } private var isUpdating = false @@ -458,7 +582,8 @@ final class StarsStatisticsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(balanceInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -522,73 +647,118 @@ final class StarsStatisticsScreenComponent: Component { } contentHeight += balanceSize.height - contentHeight += 27.0 - - let transactionsHeaderSize = self.transactionsHeader.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Stars_BotRevenue_Transactions_Title.uppercased(), - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: availableSize - ) - let transactionsHeaderFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 32.0, y: contentHeight), size: transactionsHeaderSize) - if let transactionsHeaderView = self.transactionsHeader.view { - if transactionsHeaderView.superview == nil { - self.scrollView.addSubview(transactionsHeaderView) + contentHeight += 44.0 + + var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] + if "".isEmpty { + let allTransactionsContext: StarsTransactionsContext + if let current = self.allTransactionsContext { + allTransactionsContext = current + } else { + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .all) + self.allTransactionsContext = allTransactionsContext + } + + let incomingTransactionsContext: StarsTransactionsContext + if let current = self.incomingTransactionsContext { + incomingTransactionsContext = current + } else { + incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsTransactionsContext(allTransactionsContext), mode: .incoming) + self.incomingTransactionsContext = incomingTransactionsContext } - transition.setFrame(view: transactionsHeaderView, frame: transactionsHeaderFrame) + + let outgoingTransactionsContext: StarsTransactionsContext + if let current = self.outgoingTransactionsContext { + outgoingTransactionsContext = current + } else { + outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsTransactionsContext(allTransactionsContext), mode: .outgoing) + self.outgoingTransactionsContext = outgoingTransactionsContext + } + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: environment.strings.Stars_Intro_AllTransactions, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: allTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: environment.strings.Stars_Intro_Incoming, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: incomingTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: environment.strings.Stars_Intro_Outgoing, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: outgoingTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) } - contentHeight += transactionsHeaderSize.height - contentHeight += 6.0 - self.transactionsBackground.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - self.transactionsBackground.layer.cornerRadius = 11.0 - if #available(iOS 13.0, *) { - self.transactionsBackground.layer.cornerCurve = .continuous + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } } - let transactionsSize = self.transactionsView.update( - transition: .immediate, - component: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - transactionsContext: component.transactionsContext, - isAccount: false, - action: { transaction in - component.openTransaction(transaction) - } - )), - environment: { - StarsTransactionsPanelEnvironment( + let panelTransition = transition + if !panelItems.isEmpty { + let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 + let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 + + let panelContainerSize = self.panelContainer.update( + transition: panelTransition, + component: AnyComponent(StarsTransactionsPanelContainerComponent( theme: environment.theme, - strings: strings, + strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), - isScrollable: false, - isCurrent: true, - externalScrollBounds: self.lastScrollBounds ?? .zero, - externalBottomOffset: self.lastBottomOffset ?? 1000 - ) - }, - containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) - ) - self.transactionsView.parentState = state - let transactionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - transactionsSize.width) / 2.0), y: contentHeight), size: transactionsSize) - if let panelContainerView = self.transactionsView.view { - if panelContainerView.superview == nil { - self.scrollContainerView.addSubview(panelContainerView) + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right + panelContainerInset), + items: panelItems, + currentPanelUpdated: { [weak self] id, transition in + guard let self else { + return + } + self.currentSelectedPanelId = id + self.state?.updated(transition: transition) + } + )), + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollContainerView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) + transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) } - transition.setFrame(view: panelContainerView, frame: transactionsFrame) + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() } - transition.setFrame(view: self.transactionsBackground, frame: transactionsFrame) - - contentHeight += transactionsSize.height - contentHeight += 31.0 self.ignoreScrolling = true @@ -624,7 +794,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { private let context: AccountContext private let peerId: EnginePeer.Id private let revenueContext: StarsRevenueStatsContext - private let transactionsContext: StarsTransactionsContext private weak var tooltipScreen: UndoOverlayController? private var timer: Foundation.Timer? @@ -633,7 +802,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { self.context = context self.peerId = peerId self.revenueContext = revenueContext - self.transactionsContext = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all) var withdrawImpl: (() -> Void)? var buyAdsImpl: (() -> Void)? @@ -643,7 +811,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { context: context, peerId: peerId, revenueContext: revenueContext, - transactionsContext: self.transactionsContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, @@ -700,13 +867,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { } let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { [weak self] c, a in self?.present(c, in: .window(.root)) - }, completion: { [weak self] url in + }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) Queue.mainQueue().after(2.0) { revenueContext.reload() - self?.transactionsContext.reload() + //TODO: + //self?.transactionsContext.reload() } }) self.present(controller, in: .window(.root)) @@ -787,9 +955,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }) } - - self.transactionsContext.loadMore() - + self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? StarsStatisticsScreenComponent.View else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 5431bef8d3b..88612faaa5d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -16,6 +16,7 @@ import AvatarNode import BundleIconComponent import PhotoResources import StarsAvatarComponent +import LottieComponent private extension StarsContext.State.Transaction { var extendedId: String { @@ -101,6 +102,12 @@ final class StarsTransactionsListPanelComponent: Component { } private final class ScrollViewImpl: UIScrollView { + var forceDecelerating = false + + override var isDecelerating: Bool { + return self.forceDecelerating || super.isDecelerating + } + override func touchesShouldCancel(in view: UIView) -> Bool { return true } @@ -110,8 +117,9 @@ final class StarsTransactionsListPanelComponent: Component { private let scrollView: ScrollViewImpl private let measureItem = ComponentView() - private var visibleItems: [String: ComponentView] = [:] - private var separatorViews: [String: UIView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + private var separatorLayers: [AnyHashable: SimpleLayer] = [:] + private var highlightLayer = SimpleLayer() private var ignoreScrolling: Bool = false @@ -144,6 +152,8 @@ final class StarsTransactionsListPanelComponent: Component { self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.highlightLayer) } required init?(coder: NSCoder) { @@ -154,6 +164,15 @@ final class StarsTransactionsListPanelComponent: Component { self.itemsDisposable?.dispose() } + func scrollToTop() -> Bool { + if self.scrollView.contentOffset.y > 0.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + return true + } else { + return false + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -162,6 +181,79 @@ final class StarsTransactionsListPanelComponent: Component { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) + if let decelerationAnimator = self.decelerationAnimator { + self.scrollView.forceDecelerating = false + self.decelerationAnimator = nil + decelerationAnimator.invalidate() + } + } + + private var decelerationAnimator: ConstantDisplayLinkAnimator? + func transferVelocity(_ velocity: CGFloat) { + if velocity <= 0.0 { + return + } + self.decelerationAnimator?.isPaused = true + let startTime = CACurrentMediaTime() + var currentOffset = self.scrollView.contentOffset + let decelerationRate: CGFloat = 0.998 + self.scrollView.forceDecelerating = true + //self.scrollViewDidEndDragging(self.scrollView, willDecelerate: true) + self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self else { + return + } + let t = CACurrentMediaTime() - startTime + var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t)) + currentOffset.y += currentVelocity + let maxOffset = strongSelf.scrollView.contentSize.height - strongSelf.scrollView.bounds.height + if currentOffset.y >= maxOffset { + currentOffset.y = maxOffset + currentVelocity = 0.0 + } + if currentOffset.y < 0.0 { + currentOffset.y = 0.0 + currentVelocity = 0.0 + } + + var didEnd = false + if abs(currentVelocity) < 0.1 { + strongSelf.decelerationAnimator?.isPaused = true + strongSelf.decelerationAnimator = nil + didEnd = true + } + var contentOffset = strongSelf.scrollView.contentOffset + contentOffset.y = floorToScreenPixels(currentOffset.y) + strongSelf.scrollView.setContentOffset(contentOffset, animated: false) + strongSelf.scrollViewDidScroll(strongSelf.scrollView) + if didEnd { + //strongSelf.scrollViewDidEndDecelerating(strongSelf.scrollView) + strongSelf.scrollView.forceDecelerating = false + } + }) + self.decelerationAnimator?.isPaused = false + } + + private var highlightedItemId: AnyHashable? + private func updateHighlightedItem(itemId: AnyHashable?) { + guard let environment = self.environment else { + return + } + if self.highlightedItemId == itemId { + return + } + let previousHighlightedItemId = self.highlightedItemId + self.highlightedItemId = itemId + + if let _ = previousHighlightedItemId, itemId == nil { + ComponentTransition.easeInOut(duration: 0.2).setBackgroundColor(layer: self.highlightLayer, color: .clear) + } + if let itemId, let itemView = self.visibleItems[itemId]?.view { + var highlightFrame = itemView.frame + highlightFrame.size.height += UIScreenPixel + self.highlightLayer.frame = highlightFrame + ComponentTransition.immediate.setBackgroundColor(layer: self.highlightLayer, color: environment.theme.list.itemHighlightedBackgroundColor) + } } private func updateScrolling(transition: ComponentTransition) { @@ -172,34 +264,34 @@ final class StarsTransactionsListPanelComponent: Component { var visibleBounds = environment.externalScrollBounds ?? self.scrollView.bounds visibleBounds = visibleBounds.insetBy(dx: 0.0, dy: -100.0) - var validIds = Set() + var validIds = Set() if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { for index in visibleItems.lowerBound ..< visibleItems.upperBound { if index >= self.items.count { continue } let item = self.items[index] - let id = item.extendedId + let id = AnyHashable(item.extendedId) validIds.insert(id) var itemTransition = transition let itemView: ComponentView - let separatorView: UIView - if let current = self.visibleItems[id], let currentSeparator = self.separatorViews[id] { + let separatorLayer: SimpleLayer + if let current = self.visibleItems[id], let currentSeparator = self.separatorLayers[id] { itemView = current - separatorView = currentSeparator + separatorLayer = currentSeparator } else { itemTransition = .immediate itemView = ComponentView() self.visibleItems[id] = itemView - separatorView = UIView() - self.separatorViews[id] = separatorView - self.scrollView.addSubview(separatorView) + separatorLayer = SimpleLayer() + self.separatorLayers[id] = separatorLayer + self.scrollView.layer.addSublayer(separatorLayer) } - separatorView.backgroundColor = environment.theme.list.itemBlocksSeparatorColor - separatorView.isHidden = index == self.items.count - 1 + separatorLayer.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor + separatorLayer.isHidden = index == self.items.count - 1 let fontBaseDisplaySize = 17.0 @@ -207,9 +299,14 @@ final class StarsTransactionsListPanelComponent: Component { let itemSubtitle: String? var itemDate: String var itemPeer = item.peer + var itemFile: TelegramMediaFile? switch item.peer { case let .peer(peer): - if let _ = item.giveawayMessageId { + if let starGift = item.starGift { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + itemSubtitle = item.count > 0 ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + itemFile = starGift.file + } else if let _ = item.giveawayMessageId { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize } else if !item.media.isEmpty { @@ -247,8 +344,13 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle } } else { - itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title - itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + if item.count > 0 && !item.flags.contains(.isRefund) { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + } else { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + } } case .premiumBot: itemTitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title @@ -256,6 +358,13 @@ final class StarsTransactionsListPanelComponent: Component { case .ads: itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + itemTitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Title + if let floodskipNumber = item.floodskipNumber { + itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else { + itemSubtitle = nil + } case .unsupported: itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title itemSubtitle = nil @@ -295,15 +404,44 @@ final class StarsTransactionsListPanelComponent: Component { ))) ) if let itemSubtitle { - titleComponents.append( - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + let subtitleComponent: AnyComponent + if let itemFile { + subtitleComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(LottieComponent( + content: LottieComponent.ResourceContent( + context: component.context, + file: itemFile, + attemptSynchronously: false, + providesPlaceholder: true + ), + color: nil, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + size: CGSize(width: 20.0, height: 20.0), + loop: false + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemSubtitle, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + )) + ))) + ], spacing: 2.0) + ) + } else { + subtitleComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: itemSubtitle, font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 - ))) + )) + } + + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: subtitleComponent) ) } titleComponents.append( @@ -332,12 +470,18 @@ final class StarsTransactionsListPanelComponent: Component { if !item.flags.contains(.isLocal) { component.action(item) } + }, + updateIsHighlighted: { [weak self] _, highlighted in + guard let self else { + return + } + self.updateHighlightedItem(itemId: highlighted ? id : nil) } )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: itemLayout.itemHeight) ) - let itemFrame = itemLayout.itemFrame(for: index) + let itemFrame = itemLayout.itemFrame(for: index).offsetBy(dx: itemLayout.containerInsets.left, dy: 0.0) if let itemComponentView = itemView.view { if itemComponentView.superview == nil { if !transition.animation.isImmediate { @@ -348,11 +492,11 @@ final class StarsTransactionsListPanelComponent: Component { itemTransition.setFrame(view: itemComponentView, frame: itemFrame) } let sideInset: CGFloat = 60.0 + environment.containerInsets.left - itemTransition.setFrame(view: separatorView, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset, height: UIScreenPixel)) + itemTransition.setFrame(layer: separatorLayer, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset - environment.containerInsets.right, height: UIScreenPixel)) } } - var removeIds: [String] = [] + var removeIds: [AnyHashable] = [] for (id, itemView) in self.visibleItems { if !validIds.contains(id) { removeIds.append(id) @@ -363,10 +507,10 @@ final class StarsTransactionsListPanelComponent: Component { } } } - for (id, separatorView) in self.separatorViews { + for (id, separatorLayer) in self.separatorLayers { if !validIds.contains(id) { - transition.setAlpha(view: separatorView, alpha: 0.0, completion: { [weak separatorView] _ in - separatorView?.removeFromSuperview() + transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in + separatorLayer?.removeFromSuperlayer() }) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift index 2b1e6896ccd..89f1e91ccb9 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift @@ -427,6 +427,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } class View: UIView, UIGestureRecognizerDelegate { + private let topPanelClippingView: UIView private let topPanelBackgroundView: UIView private let topPanelMergedBackgroundView: UIView private let topPanelSeparatorLayer: SimpleLayer @@ -436,6 +437,7 @@ final class StarsTransactionsPanelContainerComponent: Component { private weak var state: EmptyComponentState? private let panelsBackgroundLayer: SimpleLayer + private let clippingView: UIView private var visiblePanels: [AnyHashable: ComponentView] = [:] private var actualVisibleIds = Set() private var currentId: AnyHashable? @@ -443,6 +445,10 @@ final class StarsTransactionsPanelContainerComponent: Component { private var animatingTransition: Bool = false override init(frame: CGRect) { + self.topPanelClippingView = UIView() + self.topPanelClippingView.clipsToBounds = true + self.topPanelClippingView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topPanelBackgroundView = UIView() self.topPanelMergedBackgroundView = UIView() @@ -452,11 +458,16 @@ final class StarsTransactionsPanelContainerComponent: Component { self.panelsBackgroundLayer = SimpleLayer() + self.clippingView = UIView() + self.clippingView.clipsToBounds = true + super.init(frame: frame) self.layer.addSublayer(self.panelsBackgroundLayer) - self.addSubview(self.topPanelBackgroundView) - self.addSubview(self.topPanelMergedBackgroundView) + self.addSubview(self.clippingView) + self.addSubview(self.topPanelClippingView) + self.topPanelClippingView.addSubview(self.topPanelBackgroundView) + self.topPanelClippingView.addSubview(self.topPanelMergedBackgroundView) self.layer.addSublayer(self.topPanelSeparatorLayer) let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in @@ -593,6 +604,19 @@ final class StarsTransactionsPanelContainerComponent: Component { transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) } + func transferVelocity(_ velocity: CGFloat) { + if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View { + currentPanelView.transferVelocity(velocity) + } + } + + func scrollToTop() -> Bool { + if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View { + return currentPanelView.scrollToTop() + } + return false + } + func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[StarsTransactionsPanelContainerEnvironment.self].value @@ -610,13 +634,17 @@ final class StarsTransactionsPanelContainerComponent: Component { let topPanelCoverHeight: CGFloat = 10.0 - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) - transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) - transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame) + let containerWidth = availableSize.width - component.insets.left - component.insets.right + let topPanelFrame = CGRect(origin: CGPoint(x: component.insets.left, y: -topPanelCoverHeight), size: CGSize(width: containerWidth, height: 44.0)) + transition.setFrame(view: self.topPanelClippingView, frame: topPanelFrame) + transition.setFrame(view: self.topPanelBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size)) + transition.setFrame(view: self.topPanelMergedBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size)) + + transition.setCornerRadius(layer: self.topPanelClippingView.layer, cornerRadius: component.insets.left > 0.0 ? 11.0 : 0.0) - transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: availableSize.height - topPanelFrame.maxY))) - transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: UIScreenPixel))) if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { self.currentId = nil @@ -641,7 +669,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } } - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + component.insets.left let condensedPanelWidth: CGFloat = availableSize.width - sideInset * 2.0 let headerSize = self.header.update( transition: transition, @@ -674,7 +702,7 @@ final class StarsTransactionsPanelContainerComponent: Component { if headerView.superview == nil { self.addSubview(headerView) } - transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: sideInset, dy: 0.0), size: headerSize)) + transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: 16.0, dy: 0.0), size: headerSize)) } let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) @@ -688,7 +716,7 @@ final class StarsTransactionsPanelContainerComponent: Component { for (id, _) in self.visiblePanels { visibleIds.insert(id) } - + var validIds = Set() if let currentIndex { var anyAnchorOffset: CGFloat = 0.0 @@ -769,7 +797,7 @@ final class StarsTransactionsPanelContainerComponent: Component { ) if let panelView = panel.view { if panelView.superview == nil { - self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + self.clippingView.addSubview(panelView) } panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in @@ -790,6 +818,11 @@ final class StarsTransactionsPanelContainerComponent: Component { } } + let clippingFrame = CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height)) + + transition.setPosition(view: self.clippingView, position: clippingFrame.center) + transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: clippingFrame.size)) + var removeIds: [AnyHashable] = [] for (id, panel) in self.visiblePanels { if !validIds.contains(id) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 658dcf5d12c..22e2aa4e501 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -120,6 +120,8 @@ final class StarsTransactionsScreenComponent: Component { private var previousVelocityM1: CGFloat = 0.0 private var previousVelocity: CGFloat = 0.0 + private var listIsExpanded = false + private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? @@ -183,20 +185,56 @@ final class StarsTransactionsScreenComponent: Component { self.stateDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.enableVelocityTracking = true } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - if self.enableVelocityTracking { - self.previousVelocityM1 = self.previousVelocity - if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { - self.previousVelocity = CGFloat(value) - } + guard !self.ignoreScrolling else { + return + } + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + self.updateScrolling(transition: .immediate) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + panelContainerView.transferVelocity(self.previousVelocityM1) } - - self.updateScrolling(transition: .immediate) } } @@ -214,6 +252,12 @@ final class StarsTransactionsScreenComponent: Component { self.previousVelocityM1 = 0.0 } } + + func scrollToTop() { + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View, !panelContainerView.scrollToTop() { + self.scrollView.setContentOffset(.zero, animated: true) + } + } private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds @@ -239,7 +283,6 @@ final class StarsTransactionsScreenComponent: Component { if let starView = self.starView.view { let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale) - headerTransition.setPosition(view: starView, position: starPosition) headerTransition.setScale(view: starView, scale: titleScale) } @@ -274,6 +317,14 @@ final class StarsTransactionsScreenComponent: Component { if let view = self.topBalanceIconView.view { view.alpha = topBalanceAlpha } + + let listIsExpanded = expansionDistanceFactor == 0.0 + if listIsExpanded != self.listIsExpanded { + self.listIsExpanded = listIsExpanded + if !self.isUpdating { + self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) + } + } } let _ = self.panelContainer.updateEnvironment( @@ -363,7 +414,7 @@ final class StarsTransactionsScreenComponent: Component { var contentHeight: CGFloat = 0.0 - let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0 + let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16.0 * 2.0 let bottomInset: CGFloat = environment.safeInsets.bottom contentHeight += environment.statusBarHeight @@ -835,13 +886,16 @@ final class StarsTransactionsScreenComponent: Component { } if !panelItems.isEmpty { + let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 + let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 + let panelContainerSize = self.panelContainer.update( transition: panelTransition, component: AnyComponent(StarsTransactionsPanelContainerComponent( theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: bottomInset, right: environment.safeInsets.right + panelContainerInset), items: panelItems, currentPanelUpdated: { [weak self] id, transition in guard let self else { @@ -860,7 +914,8 @@ final class StarsTransactionsScreenComponent: Component { if panelContainerView.superview == nil { self.scrollContainerView.addSubview(panelContainerView) } - transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) + transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) } contentHeight += panelContainerSize.height } else { @@ -1038,7 +1093,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .stars(birthdays), completion: { [weak self] peerIds in + let controller = self.context.sharedContext.makeStarsGiftController(context: self.context, birthdays: birthdays, completion: { [weak self] peerIds in guard let self, let peerId = peerIds.first else { return } @@ -1098,6 +1153,15 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { self.starsContext.load(force: false) self.subscriptionsContext.loadMore() + + self.scrollToTop = { [weak self] in + guard let self else { + return + } + if let componentView = self.node.hostView.componentView as? StarsTransactionsScreenComponent.View { + componentView.scrollToTop() + } + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD index 35f40fa69f8..1f6f521503f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", "//submodules/ConfettiEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 62a93d57162..ed5fd153961 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -732,7 +732,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, animateOut: animateOut diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD index 05069f6b5c0..b10c9199288 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index fcf4852d387..2db31ddab11 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -52,7 +52,6 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) let amountSection = Child(ListSectionComponent.self) @@ -74,15 +73,6 @@ private final class SheetContent: CombinedComponent { let sideInset: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 18.0) - - let background = background.update( - component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), - availableSize: CGSize(width: context.availableSize.width, height: 1000.0), - transition: .immediate - ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) - ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 @@ -232,7 +222,8 @@ private final class SheetContent: CombinedComponent { amountFooter = AnyComponent(MultilineTextComponent( text: .plain(amountInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -466,7 +457,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.list.blocksBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 8a79e94a43e..42537334219 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -865,6 +865,28 @@ final class StorageUsageScreenComponent: Component { self.keepScreenActiveDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.enableVelocityTracking = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index f5857b2e405..212e669dbf7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -281,7 +281,7 @@ public final class StoryContentContextImpl: StoryContentContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -329,7 +329,7 @@ public final class StoryContentContextImpl: StoryContentContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -998,8 +998,8 @@ public final class StoryContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { - selectedMedia = alternativeMedia + if let slice = stateValue.slice, let alternativeMediaValue = item.alternativeMediaList.first, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.media } @@ -1315,7 +1315,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { timestamp: itemValue.timestamp, expirationTimestamp: itemValue.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: itemValue.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: itemValue.alternativeMediaList.map(EngineMedia.init), mediaAreas: itemValue.mediaAreas, text: itemValue.text, entities: itemValue.entities, @@ -1692,8 +1692,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let alternativeMedia = item.storyItem.alternativeMedia, (!preferHighQualityStories && !item.storyItem.isMy) { - selectedMedia = alternativeMedia + if let alternativeMediaValue = item.storyItem.alternativeMediaList.first, (!preferHighQualityStories && !item.storyItem.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.storyItem.media } @@ -1820,7 +1820,7 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } @@ -2004,8 +2004,8 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine var fetchPriorityDisposable: Disposable? let selectedMedia: EngineMedia - if !preferHighQualityStories, let alternativeMedia = storyItem.alternativeMedia { - selectedMedia = alternativeMedia + if !preferHighQualityStories, let alternativeMediaValue = storyItem.alternativeMediaList.first { + selectedMedia = alternativeMediaValue } else { selectedMedia = storyItem.media } @@ -2047,7 +2047,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } @@ -2247,7 +2247,7 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2940,8 +2940,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { - selectedMedia = alternativeMedia + if let slice = stateValue.slice, let alternativeMediaValue = item.alternativeMediaList.first, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.media } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 178920de707..4d26f2e7152 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1136,7 +1136,7 @@ private final class StoryContainerScreenComponent: Component { var isSilentVideo = false if case let .file(file) = slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index fd2bef30985..c0a5f2c7c60 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -200,6 +200,7 @@ final class StoryItemContentComponent: Component { if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { if self.videoNode == nil { let videoNode = UniversalVideoNode( + accountId: component.context.account.id, postbox: component.context.account.postbox, audioSession: component.context.sharedContext.mediaManager.audioSession, manager: component.context.sharedContext.mediaManager.universalVideoManager, @@ -598,10 +599,10 @@ final class StoryItemContentComponent: Component { let selectedMedia: EngineMedia var messageMedia: EngineMedia? - if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia { - selectedMedia = alternativeMedia + if !component.preferHighQuality, !component.item.isMy, let alternativeMediaValue = component.item.alternativeMediaList.first { + selectedMedia = alternativeMediaValue - switch alternativeMedia { + switch alternativeMediaValue { case let .image(image): messageMedia = .image(image) case let .file(file): @@ -918,7 +919,7 @@ final class StoryItemContentComponent: Component { } } - let shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in + var shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in if case .link = mediaArea { return true } else if case .venue = mediaArea { @@ -928,6 +929,10 @@ final class StoryItemContentComponent: Component { } } + if component.peer.id.isTelegramNotifications { + shimmeringMediaAreas = [] + } + if !shimmeringMediaAreas.isEmpty { let mediaAreasEffectView: StoryItemLoadingEffectView if let current = self.mediaAreasEffectView { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 6d68c120bb1..53974ed8840 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2864,7 +2864,7 @@ public final class StoryItemSetContainerComponent: Component { style: .story, placeholder: inputPlaceholder, maxLength: 4096, - queryTypes: [.mention, .emoji], + queryTypes: [.mention, .hashtag, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, resetInputContents: resetInputContents, nextInputMode: { [weak self] hasText in @@ -3820,7 +3820,7 @@ public final class StoryItemSetContainerComponent: Component { isVideo = true soundAlpha = 1.0 for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true soundAlpha = 0.5 @@ -3853,7 +3853,7 @@ public final class StoryItemSetContainerComponent: Component { var isSilentVideo = false if case let .file(file) = component.slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } @@ -4339,6 +4339,7 @@ public final class StoryItemSetContainerComponent: Component { urlContext: .generic, navigationController: nextController?.navigationController as? NavigationController, forceExternal: false, + forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, @@ -5392,7 +5393,7 @@ public final class StoryItemSetContainerComponent: Component { if cover { if case let .file(file) = component.slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, _, _, coverTime) = attribute { + if case let .Video(_, _, _, _, coverTime, _) = attribute { videoPlaybackPosition = coverTime } } @@ -6961,48 +6962,70 @@ public final class StoryItemSetContainerComponent: Component { if !component.slice.effectivePeer.isService { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, a in + }, action: { [weak self] _, f in guard let self, let component = self.component, let controller = component.controller() else { return } - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions( + f(.default) + + self.isReporting = true + self.updateIsProgressPaused() + + component.context.sharedContext.makeContentReportScreen( context: component.context, - parent: controller, - contextController: c, - backAction: { _ in }, - subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), - options: options, - passthrough: true, - forceTheme: defaultDarkPresentationTheme, - isDetailedReportingVisible: { [weak self] isReporting in + subject: .stories(component.slice.effectivePeer.id, [component.slice.item.storyItem.id]), + forceDark: true, + present: { c in + controller.push(c) + }, + completion: { [weak self] in guard let self else { return } - self.isReporting = isReporting + self.isReporting = false self.updateIsProgressPaused() }, - completion: { [weak self] reason, _ in - guard let self, let component = self.component, let controller = component.controller(), let reason else { - return - } - let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() - controller.present( - UndoOverlayController( - presentationData: presentationData, - content: .emoji( - name: "PoliceCar", - text: presentationData.strings.Report_Succeed - ), - elevatedLayout: false, - blurred: true, - action: { _ in return false } - ) - , in: .current - ) - } + requestSelectMessages: nil ) + +// let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] +// presentPeerReportOptions( +// context: component.context, +// parent: controller, +// contextController: c, +// backAction: { _ in }, +// subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), +// options: options, +// passthrough: true, +// forceTheme: defaultDarkPresentationTheme, +// isDetailedReportingVisible: { [weak self] isReporting in +// guard let self else { +// return +// } +// self.isReporting = isReporting +// self.updateIsProgressPaused() +// }, +// completion: { [weak self] reason, _ in +// guard let self, let component = self.component, let controller = component.controller(), let reason else { +// return +// } +// let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() +// controller.present( +// UndoOverlayController( +// presentationData: presentationData, +// content: .emoji( +// name: "PoliceCar", +// text: presentationData.strings.Report_Succeed +// ), +// elevatedLayout: false, +// blurred: true, +// action: { _ in return false } +// ) +// , in: .current +// ) +// } +// ) }))) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index bb576287101..c28cb727880 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -580,7 +580,7 @@ final class StoryItemSetContainerSendMessage { let waveformBuffer = audio.waveform.makeBitstream() - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() @@ -791,7 +791,7 @@ final class StoryItemSetContainerSendMessage { fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false) @@ -896,7 +896,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) HapticFeedback().tap() }) @@ -1812,7 +1812,7 @@ final class StoryItemSetContainerSendMessage { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) - controller.openUrl = { [weak self] url, _, _ in + controller.openUrl = { [weak self] url, _, _, _ in guard let self else { return } @@ -2176,7 +2176,7 @@ final class StoryItemSetContainerSendMessage { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } @@ -2676,6 +2676,7 @@ final class StoryItemSetContainerSendMessage { urlContext: .chat(peerId: peerId, message: nil, updatedPresentationData: updatedPresentationData), navigationController: navigationController, forceExternal: forceExternal, + forceUpdate: false, openPeer: { [weak self, weak view] peerId, navigation in guard let self, let view, let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return @@ -2919,10 +2920,10 @@ final class StoryItemSetContainerSendMessage { } if !hashtag.isEmpty { if peerName == nil { - let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(hashtag), listContext: nil) + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(nil, hashtag), listContext: nil) navigationController.pushViewController(searchController) } else { - let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) + let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, stories: true, forceDark: true) navigationController.pushViewController(searchController) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index ccc85856e55..5c2a5904280 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -1114,11 +1114,11 @@ final class StoryItemSetViewListComponent: Component { let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") }) let text: String - if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") { + if self.configuration.listMode == .everyone && ((self.query ?? "") == "") { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { if emptyButton == nil { if let views = component.storyItem.views, views.seenCount > 0 { - text = component.strings.Story_Views_ViewsNotRecorded + text = component.peerId.isGroupOrChannel ? component.strings.Story_Views_NoReactions : component.strings.Story_Views_ViewsNotRecorded } else { text = component.strings.Story_Views_ViewsExpired } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift index bed9c3d4502..b740b464bbe 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift @@ -14,7 +14,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -34,9 +34,9 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -94,8 +94,8 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -110,7 +110,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/BUILD b/submodules/TelegramUI/Components/TabSelectorComponent/BUILD index a56ab9fe513..93d1d002855 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/BUILD +++ b/submodules/TelegramUI/Components/TabSelectorComponent/BUILD @@ -13,6 +13,9 @@ swift_library( "//submodules/Display", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TextFormat", + "//submodules/AccountContext" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index dc2815c8c02..f107fd6366f 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -3,32 +3,42 @@ import UIKit import Display import ComponentFlow import PlainButtonComponent +import MultilineTextWithEntitiesComponent +import TextFormat +import AccountContext public final class TabSelectorComponent: Component { public struct Colors: Equatable { public var foreground: UIColor public var selection: UIColor + public var simple: Bool public init( foreground: UIColor, - selection: UIColor + selection: UIColor, + simple: Bool = false ) { self.foreground = foreground self.selection = selection + self.simple = simple } } public struct CustomLayout: Equatable { public var font: UIFont public var spacing: CGFloat + public var innerSpacing: CGFloat? public var lineSelection: Bool public var verticalInset: CGFloat + public var allowScroll: Bool - public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) { + public init(font: UIFont, spacing: CGFloat, innerSpacing: CGFloat? = nil, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) { self.font = font self.spacing = spacing + self.innerSpacing = innerSpacing self.lineSelection = lineSelection self.verticalInset = verticalInset + self.allowScroll = allowScroll } } @@ -45,6 +55,7 @@ public final class TabSelectorComponent: Component { } } + public let context: AccountContext? public let colors: Colors public let customLayout: CustomLayout? public let items: [Item] @@ -53,6 +64,7 @@ public final class TabSelectorComponent: Component { public let transitionFraction: CGFloat? public init( + context: AccountContext? = nil, colors: Colors, customLayout: CustomLayout? = nil, items: [Item], @@ -60,6 +72,7 @@ public final class TabSelectorComponent: Component { setSelectedId: @escaping (AnyHashable) -> Void, transitionFraction: CGFloat? = nil ) { + self.context = context self.colors = colors self.customLayout = customLayout self.items = items @@ -69,6 +82,9 @@ public final class TabSelectorComponent: Component { } public static func ==(lhs: TabSelectorComponent, rhs: TabSelectorComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } if lhs.colors != rhs.colors { return false } @@ -142,16 +158,19 @@ public final class TabSelectorComponent: Component { verticalInset = customLayout.verticalInset * 2.0 } - let innerInset: CGFloat = 12.0 - let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 + var innerInset: CGFloat = component.customLayout?.innerSpacing ?? 12.0 + var spacing: CGFloat = component.customLayout?.spacing ?? 2.0 let itemFont: UIFont var isLineSelection = false + let allowScroll: Bool if let customLayout = component.customLayout { itemFont = customLayout.font isLineSelection = customLayout.lineSelection + allowScroll = customLayout.allowScroll || component.items.count > 3 } else { itemFont = Font.semibold(14.0) + allowScroll = true } if selectionColorUpdated { @@ -169,15 +188,14 @@ public final class TabSelectorComponent: Component { } } - var contentWidth: CGFloat = spacing - var previousBackgroundRect: CGRect? - var selectedBackgroundRect: CGRect? - var nextBackgroundRect: CGRect? + var innerContentWidth: CGFloat = 0.0 let selectedIndex = component.items.firstIndex(where: { $0.id == component.selectedId }) var validIds: [AnyHashable] = [] var index = 0 + var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:] + for item in component.items { var itemTransition = transition let itemView: VisibleItem @@ -211,6 +229,7 @@ public final class TabSelectorComponent: Component { transition: .immediate, component: AnyComponent(PlainButtonComponent( content: AnyComponent(ItemComponent( + context: component.context, text: item.title, font: itemFont, color: component.colors.foreground, @@ -230,8 +249,27 @@ public final class TabSelectorComponent: Component { environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) - - if !contentWidth.isZero { + innerContentWidth += itemSize.width + itemViews[item.id] = (itemView, itemSize, itemTransition) + index += 1 + } + + let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * (spacing + innerInset)) + if estimatedContentWidth > availableSize.width && !allowScroll { + spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) + innerInset = 0.0 + } + + var contentWidth: CGFloat = spacing + var previousBackgroundRect: CGRect? + var selectedBackgroundRect: CGRect? + var nextBackgroundRect: CGRect? + + for item in component.items { + guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else { + continue + } + if contentWidth > spacing { contentWidth += spacing } let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) @@ -254,9 +292,8 @@ public final class TabSelectorComponent: Component { } itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin) itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size)) - itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4) + itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4) } - index += 1 } contentWidth += spacing @@ -288,7 +325,7 @@ public final class TabSelectorComponent: Component { } } - var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: 12.0, dy: 0.0) + var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: innerInset, dy: 0.0) mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0 mappedSelectionFrame.size.height = 3.0 transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame) @@ -302,7 +339,7 @@ public final class TabSelectorComponent: Component { self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0) self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width - if let selectedBackgroundRect { + if let selectedBackgroundRect, self.bounds.width > 0.0 { self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false) } @@ -331,6 +368,7 @@ extension CGRect { } private final class ItemComponent: CombinedComponent { + let context: AccountContext? let text: String let font: UIFont let color: UIColor @@ -338,12 +376,14 @@ private final class ItemComponent: CombinedComponent { let selectionFraction: CGFloat init( + context: AccountContext?, text: String, font: UIFont, color: UIColor, selectedColor: UIColor, selectionFraction: CGFloat ) { + self.context = context self.text = text self.font = font self.color = color @@ -352,6 +392,9 @@ private final class ItemComponent: CombinedComponent { } static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } if lhs.text != rhs.text { return false } @@ -371,17 +414,25 @@ private final class ItemComponent: CombinedComponent { } static var body: Body { - let title = Child(Text.self) - let selectedTitle = Child(Text.self) + let title = Child(MultilineTextWithEntitiesComponent.self) + let selectedTitle = Child(MultilineTextWithEntitiesComponent.self) return { context in let component = context.component - + + let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color) + var range = (attributedTitle.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + } + let title = title.update( - component: Text( - text: component.text, - font: component.font, - color: component.color + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context?.animationCache, + animationRenderer: component.context?.animationRenderer, + placeholderColor: .white, + text: .plain(attributedTitle) ), availableSize: context.availableSize, transition: .immediate @@ -391,11 +442,19 @@ private final class ItemComponent: CombinedComponent { .opacity(1.0 - component.selectionFraction) ) + let selectedAttributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.selectedColor) + range = (selectedAttributedTitle.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + } + let selectedTitle = selectedTitle.update( - component: Text( - text: component.text, - font: component.font, - color: component.selectedColor + component: MultilineTextWithEntitiesComponent( + context: nil, + animationCache: nil, + animationRenderer: nil, + placeholderColor: .white, + text: .plain(selectedAttributedTitle) ), availableSize: context.availableSize, transition: .immediate diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 12a9161dfd1..35c5a4e6c3c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -741,6 +741,9 @@ public final class TextFieldComponent: Component { } self.insertText(NSAttributedString(string: insertString)) + } else if (range.length == 0 && text == "\n"), let returnKeyAction = component.returnKeyAction { + returnKeyAction() + return false } return false } diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 14194961617..dc3db471197 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -299,7 +299,7 @@ public class ImmediateTextNodeWithEntities: TextNode { public var arguments: TextNodeWithEntities.Arguments? private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] - private var dustNode: InvisibleInkDustNode? + public private(set) var dustNode: InvisibleInkDustNode? public var visibility: Bool = false { didSet { diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index af68fabbce8..4f80bea7c1a 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1231,7 +1231,7 @@ public class VideoMessageCameraScreen: ViewController { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileReferenceMediaResource(localFilePath: path, randomId: id) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: Int64(data.count), attributes: [.FileName(fileName: "video.mp4")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: Int64(data.count), attributes: [.FileName(fileName: "video.mp4")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: self.context.engine.account, peerId: self.context.engine.account.peerId, messages: [message]).start() @@ -1887,7 +1887,7 @@ public class VideoMessageCameraScreen: ViewController { context.account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] if self.cameraState.isViewOnceEnabled { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json new file mode 100644 index 00000000000..719c35fbb0a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tagrecent.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf new file mode 100644 index 00000000000..62a616bc674 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json new file mode 100644 index 00000000000..ff2c7d7a8b3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "copy_10.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf new file mode 100644 index 00000000000..e87c212a72e Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json new file mode 100644 index 00000000000..3f3ed2761d3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftline_chat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf new file mode 100644 index 00000000000..3ef14939971 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json new file mode 100644 index 00000000000..4999eeb4319 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "edited.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf new file mode 100644 index 00000000000..129aee2ff4a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json new file mode 100644 index 00000000000..76ccbf54aa7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Improving_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf new file mode 100644 index 00000000000..afdcc8bf28b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json new file mode 100644 index 00000000000..cc44172ecd8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettings_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf new file mode 100644 index 00000000000..028e8b9ab05 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json new file mode 100644 index 00000000000..b00344b3dbe --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "settings_24 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf new file mode 100644 index 00000000000..ec496f75d61 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json new file mode 100644 index 00000000000..e2fa1abb904 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettingsauto_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf new file mode 100644 index 00000000000..d36b2d76865 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json new file mode 100644 index 00000000000..1f7bfc2bb2c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettingshd_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf new file mode 100644 index 00000000000..735db423a85 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json new file mode 100644 index 00000000000..8f71976b5fa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettingssd_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf new file mode 100644 index 00000000000..c77db5874e0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json new file mode 100644 index 00000000000..9c81bdcc4bb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf new file mode 100644 index 00000000000..c3ed904f784 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json new file mode 100644 index 00000000000..126065aee33 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hidden_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf new file mode 100644 index 00000000000..7ff4831d791 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json new file mode 100644 index 00000000000..bc85fccee13 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftline_cell.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf new file mode 100644 index 00000000000..68d0f7b17d2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json new file mode 100644 index 00000000000..84dca5328aa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "paidbroadcast.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf new file mode 100644 index 00000000000..49f42001935 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json new file mode 100644 index 00000000000..923ba188ab6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gift_30 (4).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf new file mode 100644 index 00000000000..0d6d36c6032 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json new file mode 100644 index 00000000000..6a409d155b6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unlock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf new file mode 100644 index 00000000000..67593f36987 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json new file mode 100644 index 00000000000..e582efcfd30 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bot_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf new file mode 100644 index 00000000000..e682166cc8a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json new file mode 100644 index 00000000000..a7e458b55c3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf new file mode 100644 index 00000000000..00748a0e19b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs b/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs index 62a21279fb7..1cde51426d6 100644 Binary files a/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs and b/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json b/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json new file mode 100644 index 00000000000..a6a099f10f3 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":512,"h":512,"nm":"SpeedUp","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"L","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-149.676,-134.674,0],"ix":2,"l":2},"a":{"a":0,"k":[-149.676,-134.674,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-118.501,-90.586],[-180.85,-131.391],[-120.298,-178.762]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"R 2","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0]},{"t":37,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[148.867,-138.259,0],"ix":2,"l":2},"a":{"a":0,"k":[86.867,-138.259,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.693,-182.348],[118.041,-141.542],[57.489,-94.171]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":238,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":15.525,"s":[315.867,117.741,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[386.867,113.741,0],"to":[0,0,0],"ti":[0,0,0]},{"t":58,"s":[361.867,113.741,0]}],"ix":2,"l":2},"a":{"a":0,"k":[86.867,-138.259,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.693,-182.348],[118.041,-141.542],[57.489,-94.171]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":238,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Finger","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":15,"s":[-8]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":40.369,"s":[2]},{"t":59,"s":[0]}],"ix":10},"p":{"a":0,"k":[-18.5,36.537,0],"ix":2,"l":2},"a":{"a":0,"k":[-18.5,36.537,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0.184,-29.147],[0,0],[0,0]],"o":[[0,0],[0,0],[0.179,-28.358],[0,0],[0,0],[0,0]],"v":[[-42.122,74.94],[-42.656,-41.628],[-42.954,-106.447],[2.666,-106.159],[2.179,-28.956],[1.757,30.035]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Hand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":15,"s":[-27]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":39,"s":[-4]},{"t":58,"s":[-8]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.788,"y":1},"o":{"x":0.675,"y":0.1},"t":15,"s":[324.228,369.807,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.2,"y":0},"t":35.711,"s":[332.157,343.048,0],"to":[0,0,0],"ti":[0,0,0]},{"t":54.341796875,"s":[331.157,340.048,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28,102.537,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.8,"y":1},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[12.251,16.781],[45.025,26.208],[-30.441,-3.454],[-8.116,-7.778]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.42,-29.674],[-23.406,-13.363],[30.592,3.823],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.145,150.224],[-124.225,81.972],[-99.575,44.059],[-41.999,74.829]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.2,"y":0},"t":29.5,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[15.266,14.266],[43.037,9.57],[-40.297,5.526],[-13.417,-4.962]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.287,-19.893],[-27.722,-6.164],[30.686,-4.208],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.531,154.659],[-124.202,111.503],[-111.986,60.033],[-41.999,74.829]],"c":false}]},{"t":48.130859375,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[11.838,17.125],[45.297,28.484],[-29.093,-4.682],[-7.391,-8.163]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.438,-31.012],[-22.816,-14.347],[30.58,4.921],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.092,149.617],[-124.228,77.933],[-97.877,41.873],[-41.999,74.829]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.19,-30.193],[0,0]],"o":[[0,0],[0.19,-30.193],[0,0],[0,0]],"v":[[44.474,37.139],[44.686,3.459],[90.983,3.751],[90.706,47.786]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.288,-28.763],[0,0]],"o":[[0,0],[0.181,-28.763],[0,0],[0,0]],"v":[[1.812,29.305],[2.067,-11.134],[44.776,-10.865],[44.474,37.139]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0}],"markers":[{"tm":0,"cm":"1","dr":0},{"tm":38,"cm":"2","dr":0}],"props":{}} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html b/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html new file mode 100755 index 00000000000..26a5e4e76cc --- /dev/null +++ b/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html @@ -0,0 +1,197 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/WebEmbed/hls.js b/submodules/TelegramUI/Resources/WebEmbed/hls.js new file mode 100644 index 00000000000..b3ac6dd0551 --- /dev/null +++ b/submodules/TelegramUI/Resources/WebEmbed/hls.js @@ -0,0 +1,2 @@ +!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function v(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(m);var p=m.exports,y=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},E=Number.isSafeInteger||function(t){return"number"==typeof t&&Math.abs(t)<=T},T=Number.MAX_SAFE_INTEGER||9007199254740991,S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.MAX_AUTO_LEVEL_UPDATED="hlsMaxAutoLevelUpdated",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t.STEERING_MANIFEST_LOADED="hlsSteeringManifestLoaded",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),A=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),R=function(){},b={trace:R,debug:R,log:R,warn:R,info:R,error:R},k=b;function D(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i"):R}(e)}))}function I(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){D(t,"debug","log","info","warn","error");try{k.log('Debug logs enabled for "'+e+'" in hls.js version 1.5.15')}catch(t){k=b}}else k=b}var w=k,C=/^(\d+)x(\d+)$/,_=/(.+?)=(".*?"|.*?)(?:,|$)/g,x=function(){function t(e){"string"==typeof e&&(e=t.parseAttrList(e)),o(this,e)}var e=t.prototype;return e.decimalInteger=function(t){var e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},s(t,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(t){return"X-"===t.substring(0,2)}))}}]),t}();function P(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new x({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);y(n.getTime())&&(this._endDate=n)}}return s(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(y(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&y(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r,i=t.split("@",2);r=1===i.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(i[1]),this._byteRange=[r,parseInt(i[0])+r]},s(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=p.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.urlId=0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},s(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!y(this.programDateTime))return null;var t=y(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),s(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},s(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&y(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j="undefined"!=typeof self?self:void 0,q={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},X="org.w3.clearkey",z="com.apple.streamingkeydelivery",Q="com.microsoft.playready",J="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case z:return q.FAIRPLAY;case Q:return q.PLAYREADY;case J:return q.WIDEVINE;case X:return q.CLEARKEY}}var Z="1077efecc0b24d02ace33c1e52e2fb4b",tt="e2719d58a985b3c9781ab030af78d30e",et="9a04f07998404286ab92e65be0885f95",rt="edef8ba979d64acea3c827dcd51d21ed";function it(t){return t===rt?q.WIDEVINE:t===et?q.PLAYREADY:t===Z||t===tt?q.CLEARKEY:void 0}function nt(t){switch(t){case q.FAIRPLAY:return z;case q.PLAYREADY:return Q;case q.WIDEVINE:return J;case q.CLEARKEY:return X}}function at(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[q.FAIRPLAY,q.WIDEVINE,q.PLAYREADY,q.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[q.WIDEVINE]&&r&&i.push(q.WIDEVINE),i}var st,ot=null!=j&&null!=(st=j.navigator)&&st.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function lt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var ut,ht=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},dt=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},ct=function(t,e){for(var r=e,i=0;ht(t,e);)i+=10,i+=ft(t,e+6),dt(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ft=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},gt=function(t,e){return ht(t,e)&&ft(t,e+6)+10<=t.length-e},vt=function(t){for(var e=yt(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function bt(){if(!navigator.userAgent.includes("PlayStation 4"))return ut||void 0===self.TextDecoder||(ut=new self.TextDecoder("utf-8")),ut}var kt={hexDump:function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function Ot(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(Ct(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=Ot(t.subarray(n+8,s),e.slice(1));o.length&&It.apply(r,o)}n=s}return r}function Nt(t){var e=[],r=t[0],i=8,n=xt(t,i);i+=4;var a=0,s=0;0===r?(a=xt(t,i),s=xt(t,i+4),i+=8):(a=Pt(t,i),s=Pt(t,i+8),i+=16),i+=2;var o=t.length+s,l=_t(t,i);i+=2;for(var u=0;u>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var f=xt(t,h);h+=4,e.push({referenceSize:c,subsegmentDuration:f,info:{duration:f/n,start:o,end:o+c-1}}),o+=c,i=h+=4}return{earliestPresentationTime:a,timescale:n,version:r,referencesCount:l,references:e}}function Ut(t){for(var e=[],r=Ot(t,["moov","trak"]),n=0;n12){var h=4;if(3!==u[h++])break;h=Gt(u,h),h+=2;var d=u[h++];if(128&d&&(h+=2),64&d&&(h+=u[h++]),4!==u[h++])break;h=Gt(u,h);var c=u[h++];if(64!==c)break;if(n+="."+Kt(c),h+=12,5!==u[h++])break;h=Gt(u,h);var f=u[h++],g=(248&f)>>3;31===g&&(g+=1+((7&f)<<3)+((224&u[h])>>5)),n+="."+g}break;case"hvc1":case"hev1":var v=Ot(r,["hvcC"])[0],m=v[1],p=["","A","B","C"][m>>6],y=31&m,E=xt(v,2),T=(32&m)>>5?"H":"L",S=v[12],L=v.subarray(6,12);n+="."+p+y,n+="."+E.toString(16).toUpperCase(),n+="."+T+S;for(var A="",R=L.length;R--;){var b=L[R];(b||A)&&(A="."+b.toString(16).toUpperCase()+A)}n+=A;break;case"dvh1":case"dvhe":var k=Ot(r,["dvcC"])[0],D=k[2]>>1&127,I=k[2]<<5&32|k[3]>>3&31;n+="."+Ht(D)+"."+Ht(I);break;case"vp09":var w=Ot(r,["vpcC"])[0],C=w[4],_=w[5],x=w[6]>>4&15;n+="."+Ht(C)+"."+Ht(_)+"."+Ht(x);break;case"av01":var P=Ot(r,["av1C"])[0],F=P[1]>>>5,M=31&P[1],O=P[2]>>>7?"H":"M",N=(64&P[2])>>6,U=(32&P[2])>>5,B=2===F&&N?U?12:10:N?10:8,G=(16&P[2])>>4,K=(8&P[2])>>3,H=(4&P[2])>>2,V=3&P[2];n+="."+F+"."+Ht(M)+O+"."+Ht(B)+"."+G+"."+K+H+V+"."+Ht(1)+"."+Ht(1)+"."+Ht(1)+".0"}return{codec:n,encrypted:a}}function Gt(t,e){for(var r=e+5;128&t[e++]&&e>1&63;return 39===r||40===r}return 6==(31&e)}function Xt(t,e,r,i){var n=zt(t),a=0;a+=e;for(var s=0,o=0,l=0;a=n.length)break;s+=l=n[a++]}while(255===l);o=0;do{if(a>=n.length)break;o+=l=n[a++]}while(255===l);var u=n.length-a,h=a;if(ou){w.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[h++]){var d=_t(n,h);if(h+=2,49===d){var c=xt(n,h);if(h+=4,1195456820===c){var f=n[h++];if(3===f){var g=n[h++],v=64&g,m=v?2+3*(31&g):0,p=new Uint8Array(m);if(v){p[0]=g;for(var y=1;y16){for(var E=[],T=0;T<16;T++){var S=n[h++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var L=o-16,A=new Uint8Array(L),R=0;R0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>>24;if(0!==n&&1!==n)return{offset:r,size:e};var a=t.buffer,s=kt.hexDump(new Uint8Array(a,r+12,16)),o=t.getUint32(28),l=null,u=null;if(0===n){if(e-32>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case J:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case Q:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Qt(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=$t[this.uri];if(!f){var g=Object.keys($t).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),$t[this.uri]=f}this.keyId=f}return this},t}(),te=/\{\$([a-zA-Z0-9-_]+)\}/g;function ee(t){return te.test(t)}function re(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=ie(t,a))}}function ie(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(te,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function ne(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function ae(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function se(t){if(void 0===t&&(t=!0),"undefined"!=typeof self)return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}var oe={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function le(t,e,r){return void 0===r&&(r=!0),!t.split(",").some((function(t){return!ue(t,e,r)}))}function ue(t,e,r){var i;void 0===r&&(r=!0);var n=se(r);return null!=(i=null==n?void 0:n.isTypeSupported(he(t,e)))&&i}function he(t,e){return e+'/mp4;codecs="'+t+'"'}function de(t){if(t){var e=t.substring(0,4);return oe.video[e]}return 2}function ce(t){return t.split(",").reduce((function(t,e){var r=oe.video[e];return r?(2*r+t)/(t?3:2):(oe.audio[e]+t)/(t?2:1)}),0)}var fe={},ge=/flac|opus/i;function ve(t,e){return void 0===e&&(e=!0),t.replace(ge,(function(t){return function(t,e){if(void 0===e&&(e=!0),fe[t])return fe[t];for(var r={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t],i=0;i0&&a.length0&&X.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=X.optionalFloat("PART-HOLD-BACK",0),h.holdBack=X.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var z=new x(I);h.partTarget=z.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=h.partList;Q||(Q=h.partList=[]);var J=g>0?Q[Q.length-1]:void 0,$=g++,Z=new x(I);re(h,Z,["BYTERANGE","URI"]);var tt=new K(Z,E,e,$,J);Q.push(tt),E.duration+=tt.duration;break;case"PRELOAD-HINT":var et=new x(I);re(h,et,["URI"]),h.preloadHint=et;break;case"RENDITION-REPORT":var rt=new x(I);re(h,rt,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(rt);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(De(E,p),E.cc=m,h.fragmentHint=E,u&&we(E,u,h));var it=d.length,nt=d[0],at=d[it-1];if((v+=h.skippedSegments*h.targetduration)>0&&it&&at){h.averagetargetduration=v/it;var st=at.sn;h.endSN="initSegment"!==st?st:0,h.live||(at.endList=!0),nt&&(h.startCC=nt.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function Ae(t,e,r){var i,n,a=new x(t);re(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?Le.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new Zt(s,d,h,c,l)}function Re(t){var e=new x(t).decimalFloatingPoint("TIME-OFFSET");return y(e)?e:null}function be(t,e){var r=(t||"").split(/[ ,]+/).filter((function(t){return t}));["video","audio","text"].forEach((function(t){var i=r.filter((function(e){return function(t,e){var r=oe[e];return!!r&&!!r[t.slice(0,4)]}(e,t)}));i.length&&(e[t+"Codec"]=i.join(","),r=r.filter((function(t){return-1===i.indexOf(t)})))})),e.unknownCodecs=r}function ke(t,e,r){var i=e[r];i&&(t[r]=i)}function De(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),y(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Ie(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function we(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var Ce="manifest",_e="level",xe="audioTrack",Pe="subtitleTrack",Fe="main",Me="audio",Oe="subtitle";function Ne(t){switch(t.type){case xe:return Me;case Pe:return Oe;default:return Fe}}function Ue(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Be=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Ce,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.pathwayId,a=e.url,s=e.deliveryDirectives;this.load({id:r,level:i,pathwayId:n,responseType:"text",type:_e,url:a,deliveryDirectives:s})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:xe,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:Pe,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url&&l.level===t.level)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===Ce?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),y(null==(e=t.deliveryDirectives)?void 0:e.part)&&(t.type===_e&&null!==t.level?i=this.hls.levels[t.level].details:t.type===xe&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===Pe&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),Le.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=Ue(t,r),o=Le.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=Le.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new x({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=Ue(t,r),h=y(o)?o:y(s)?s:0,d=Ne(r),c=Le.parseLevelPlaylist(t.data,u,h,d,0,this.variableList);if(l===Ce){var f={attrs:new x({}),bitrate:0,details:c,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[f],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=c,this.handlePlaylistLoaded(c,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.MANIFEST_PARSING_ERROR,fatal:e.type===Ce,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===_e?s+=": "+t.level+" id: "+t.id:t.type!==xe&&t.type!==Pe||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=A.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case Ce:l=r?A.MANIFEST_LOAD_TIMEOUT:A.MANIFEST_LOAD_ERROR,u=!0;break;case _e:l=r?A.LEVEL_LOAD_TIMEOUT:A.LEVEL_LOAD_ERROR,u=!1;break;case xe:l=r?A.AUDIO_TRACK_LOAD_TIMEOUT:A.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case Pe:l=r?A.SUBTITLE_TRACK_LOAD_TIMEOUT:A.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=Ue(e,i),f=Ne(i),g="number"==typeof i.level&&f===Fe?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Ce:case _e:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case xe:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case Pe:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Ge(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Ke(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function He(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Ve(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sQe&&(d=Qe),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&(!t||e.startDate.05&&this.forwardBufferLength>1){var l=Math.min(2,Math.max(1,a)),u=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(l,Math.max(1,u))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},s(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),tr=["NONE","TYPE-0","TYPE-1",null],er=["SDR","PQ","HLG"],rr="",ir="YES",nr="v2";function ar(t){var e=t.canSkipUntil,r=t.canSkipDateRanges,i=t.age;return e&&it.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function hr(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(y(l)){var h=Math.abs(l-r);y(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)ur(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||fr(e,i[r].start)}function fr(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(n)||!!r);return t.shouldRetry?t.shouldRetry(t,e,r,i,a):a}var Lr=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function Ar(t,e,r,i,n){void 0===r&&(r=0),void 0===i&&(i=0),void 0===n&&(n=.005);var a=null;if(t){a=e[t.sn-e[0].sn+1]||null;var s=t.endDTS-r;s>0&&s<15e-7&&(r+=15e-7)}else 0===r&&0===e[0].start&&(a=e[0]);if(a&&((!t||t.level===a.level)&&0===Rr(r,i,a)||function(t,e,r){if(e&&0===e.start&&e.level0){var i=e.tagList.reduce((function(t,e){return"INF"===e[0]&&(t+=parseFloat(e[1])),t}),r);return t.start<=i}return!1}(a,t,Math.min(n,i))))return a;var o=Lr(e,Rr.bind(null,r,i));return!o||o===t&&a?a:o}function Rr(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function br(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var kr=0,Dr=2,Ir=3,wr=5,Cr=0,_r=1,xr=2,Pr=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){},e.stopLoad=function(){this.playlistError=0},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===Fe?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case A.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:kr,flags:Cr});case A.FRAG_GAP:case A.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Dr);case A.LEVEL_EMPTY_ERROR:case A.LEVEL_PARSING_ERROR:var s,o,l=e.parent===Fe?e.level:n.loadLevel;return void(e.details===A.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case A.AUDIO_TRACK_LOAD_ERROR:case A.AUDIO_TRACK_LOAD_TIMEOUT:case A.SUBTITLE_LOAD_ERROR:case A.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===xe&&u.hasAudioGroup(a.groupId)||a.type===Pe&&u.hasSubtitleGroup(a.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=Dr,void(e.errorAction.flags=_r)}return;case A.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d?e.errorAction={action:Dr,flags:xr,hdcpLevel:d}:this.keySystemError(e));case A.BUFFER_ADD_CODEC_ERROR:case A.REMUX_ALLOC_ERROR:case A.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case A.INTERNAL_EXCEPTION:case A.BUFFER_APPENDING_ERROR:case A.BUFFER_FULL_ERROR:case A.LEVEL_SWITCH_ERROR:case A.BUFFER_STALLED_ERROR:case A.BUFFER_SEEK_OVER_HOLE:case A.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:kr,flags:Cr})}e.type===L.KEY_SYSTEM_ERROR&&this.keySystemError(e)}},e.keySystemError=function(t){var e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)},e.getPlaylistRetryOrSwitchAction=function(t,e){var r=yr(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(Sr(r,i,pr(t),t.response))return{action:wr,flags:Cr,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(t,e);return r&&(n.retryConfig=r,n.retryCount=i),n},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=yr(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i&&(t.details!==A.FRAG_GAP&&i.fragmentError++,Sr(o,l,pr(t),t.response)))return{action:wr,flags:Cr,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(t,r);return o&&(u.retryConfig=o,u.retryCount=l),u},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i){var n,a,s=t.details;i.loadError++,s===A.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,h=r.minAutoLevel,d=r.maxAutoLevel;r.autoLevelEnabled||(r.loadLevel=-1);for(var c,f=null==(n=t.frag)?void 0:n.type,g=(f===Me&&s===A.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR))&&l.some((function(t){var e=t.audioCodec;return i.audioCodec!==e})),v="video"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR)&&l.some((function(t){var e=t.codecSet,r=t.audioCodec;return i.codecSet!==e&&i.audioCodec===r})),m=null!=(a=t.context)?a:{},p=m.type,y=m.groupId,E=function(){var e=(T+u)%l.length;if(e!==u&&e>=h&&e<=d&&0===l[e].loadError){var r,n,a=l[e];if(s===A.FRAG_GAP&&f===Fe&&t.frag){var c=l[e].details;if(c){var m=Ar(t.frag,c.fragments,t.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===xe&&a.hasAudioGroup(y)||p===Pe&&a.hasSubtitleGroup(y))return 0;if(f===Me&&null!=(r=i.audioGroups)&&r.some((function(t){return a.hasAudioGroup(t)}))||f===Oe&&null!=(n=i.subtitleGroups)&&n.some((function(t){return a.hasSubtitleGroup(t)}))||g&&i.audioCodec===a.audioCodec||!g&&i.audioCodec!==a.audioCodec||v&&i.codecSet===a.codecSet)return 0}return o=e,1}},T=l.length;T--&&(0===(c=E())||1!==c););if(o>-1&&r.loadLevel!==o)return t.levelRetry=!0,this.playlistError=0,{action:Dr,flags:Cr,nextAutoLevel:o}}return{action:Dr,flags:_r}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case kr:break;case Dr:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===A.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn('MediaSource ended after "'+e.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case Cr:this.switchLevel(t,a);break;case xr:n&&(e.maxHdcpLevel=tr[tr.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},t}(),Fr=function(){function t(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=w.log.bind(w,e+":"),this.warn=w.warn.bind(w,e+":"),this.hls=t}var e=t.prototype;return e.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},e.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},e.startLoad=function(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()},e.stopLoad=function(){this.canLoad=!1,this.clearTimer()},e.switchParams=function(t,e,r){var i=null==e?void 0:e.renditionReports;if(i){for(var n=-1,a=0;a=0&&d>e.partTarget&&(h+=1)}var c=r&&ar(r);return new sr(u,h>=0?h:void 0,c)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&dr(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var E=Math.floor(y/n.targetduration);u+=E,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+E+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var T=this.hls.mainForwardBufferInfo,S=T?T.end-T.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=Er(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),Mr=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),Or=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Mr(t),this.fast_=new Mr(e),this.defaultTTFB_=i,this.ttfb_=new Mr(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new Mr(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Mr(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new Mr(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),Nr={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},Ur={};function Br(t,e,r){var n=t.videoCodec,a=t.audioCodec;if(!n||!a||!r)return Promise.resolve(Nr);var s={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},o=t.videoRange;"SDR"!==o&&(s.transferFunction=o.toLowerCase());var l=n.split(",").map((function(t){return{type:"media-source",video:i(i({},s),{},{contentType:he(t,"video")})}}));return a&&t.audioGroups&&t.audioGroups.forEach((function(t){var r;t&&(null==(r=e.groups[t])||r.tracks.forEach((function(e){if(e.groupId===t){var r=e.channels||"",i=parseFloat(r);y(i)&&i>2&&l.push.apply(l,a.split(",").map((function(t){return{type:"media-source",audio:{contentType:he(t,"audio"),channels:""+i}}})))}})))})),Promise.all(l.map((function(t){var e=function(t){var e=t.audio,r=t.video,i=r||e;if(i){var n=i.contentType.split('"')[1];if(r)return"r"+r.height+"x"+r.width+"f"+Math.ceil(r.framerate)+(r.transferFunction||"sd")+"_"+n+"_"+Math.ceil(r.bitrate/1e5);if(e)return"c"+e.channels+(e.spatialRendering?"s":"n")+"_"+n}return""}(t);return Ur[e]||(Ur[e]=r.decodingInfo(t))}))).then((function(t){return{supported:!t.some((function(t){return!t.supported})),configurations:l,decodingInfoResults:t}})).catch((function(t){return{supported:!1,configurations:l,decodingInfoResults:[],error:t}}))}function Gr(t,e){var r=!1,i=[];return t&&(r="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||er.slice(0),i=(r=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){var t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}())?i.filter((function(t){return"SDR"!==t})):["SDR"]),{preferHDR:r,allowedVideoRanges:i}}function Kr(t,e){w.log('[abr] start candidates with "'+t+'" ignored because '+e)}function Hr(t,e,r){if("attrs"in t){var i=e.indexOf(t);if(-1!==i)return i}for(var n=0;n-1,p=e.getBwEstimate(),E=i.levels,T=E[t.level],L=o.total||Math.max(o.loaded,Math.round(l*T.averageBitrate/8)),A=m?u-v:u;A<1&&m&&(A=Math.min(u,8*o.loaded/p));var R=m?1e3*o.loaded/A:0,b=R?(L-o.loaded)/R:8*L/p+c/1e3;if(!(b<=g)){var k,D=R?8*R:p,I=Number.POSITIVE_INFINITY;for(k=t.level-1;k>h;k--){var C=E[k].maxBitrate;if((I=e.getTimeToLoadFrag(c/1e3,D,l*C,!E[k].details))=b||I>10*l)){i.nextLoadLevel=i.nextAutoLevel=k,m?e.bwEstimator.sample(u-Math.min(c,v),o.loaded):e.bwEstimator.sampleTTFB(u);var _=E[k].maxBitrate;e.getBwEstimate()*e.hls.config.abrBandWidthUpFactor>_&&e.resetEstimator(_),e.clearTimer(),w.warn("[abr] Fragment "+t.sn+(r?" part "+r.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+g.toFixed(3)+" s\n Estimated load time for current fragment: "+b.toFixed(3)+" s\n Estimated load time for down switch fragment: "+I.toFixed(3)+" s\n TTFB estimate: "+(0|v)+" ms\n Current BW estimate: "+(y(p)?0|p:"Unknown")+" bps\n New BW estimate: "+(0|e.getBwEstimate())+" bps\n Switching to level "+k+" @ "+(0|_)+" bps"),i.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:r,stats:o})}}}}}}},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}var e=t.prototype;return e.resetEstimator=function(t){t&&(w.log("setting initial bwe to "+t),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},e.initEstimator=function(){var t=this.hls.config;return new Or(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(S.ERROR,this.onError,this))},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null},e.onManifestLoading=function(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},e.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},e.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.onError=function(t,e){if(!e.fatal)switch(e.details){case A.BUFFER_ADD_CODEC_ERROR:case A.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case A.FRAG_LOAD_TIMEOUT:var r=e.frag,i=this.fragCurrent,n=this.partCurrent;if(r&&i&&r.sn===i.sn&&r.level===i.level){var a=performance.now(),s=n?n.stats:r.stats,o=a-s.loading.start,l=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&l>-1){var u=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(o-Math.min(u,l),s.loaded)}else this.bwEstimator.sampleTTFB(o)}}},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats.loading,n=i.end-i.start;y(n)&&(this.lastLevelLoadSec=n/1e3),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===Fe&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),r.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(S.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.getBwEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},e.ignoreFragment=function(t){return t.type!==Fe||"initSegment"===t.sn},e.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},e.getAutoLevelKey=function(){return this.getBwEstimate()+"_"+this.getStarvationDelay().toFixed(2)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=e?e.duration:t?t.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay(),u=n.abrBandWidthFactor,h=n.abrBandWidthUpFactor;if(l){var d=this.findBestLevel(o,a,i,l,0,u,h);if(d>=0)return d}var c=s?Math.min(s,n.maxStarvationDelay):n.maxStarvationDelay;if(!l){var f=this.bitrateTestDelay;f&&(c=(s?Math.min(s,n.maxLoadingDelay):n.maxLoadingDelay)-f,w.info("[abr] bitrate test took "+Math.round(1e3*f)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*c)+" ms"),u=h=1)}var g=this.findBestLevel(o,a,i,l,c,u,h);if(w.info("[abr] "+(l?"rebuffering expected":"buffer is empty")+", optimal quality level "+g),g>-1)return g;var v=r.levels[a],m=r.levels[r.loadLevel];return(null==v?void 0:v.bitrate)<(null==m?void 0:m.bitrate)?a:r.loadLevel},e.getStarvationDelay=function(){var t=this.hls,e=t.media;if(!e)return 1/0;var r=e&&0!==e.playbackRate?Math.abs(e.playbackRate):1,i=t.mainForwardBufferInfo;return(i?i.len:0)/r},e.getBwEstimate=function(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate},e.findBestLevel=function(t,e,r,i,n,a,s){var o,l=this,u=i+n,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,c=this.fragCurrent,f=this.partCurrent,g=this.hls,v=g.levels,m=g.allAudioTracks,p=g.loadLevel,E=g.config;if(1===v.length)return 0;var T,S=v[d],L=!(null==S||null==(o=S.details)||!o.live),A=-1===p||-1===h,R="SDR",b=(null==S?void 0:S.frameRate)||0,k=E.audioPreference,D=E.videoPreference,I=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce((function(t,e){var r=t.groups[e.groupId];r||(r=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),r.tracks.push(e);var i=e.channels||"2";return r.channels[i]=(r.channels[i]||0)+1,r.hasDefault=r.hasDefault||e.default,r.hasAutoSelect=r.hasAutoSelect||e.autoselect,r.hasDefault&&(t.hasDefaultAudio=!0),r.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(A){if(-1!==this.firstSelection)return this.firstSelection;var C=this.codecTiers||(this.codecTiers=function(t,e,r,i){return t.slice(r,i+1).reduce((function(t,r){if(!r.codecSet)return t;var i=r.audioGroups,n=t[r.codecSet];n||(t[r.codecSet]=n={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),n.minBitrate=Math.min(n.minBitrate,r.bitrate);var a=Math.min(r.height,r.width);return n.minHeight=Math.min(n.minHeight,a),n.minFramerate=Math.min(n.minFramerate,r.frameRate),n.maxScore=Math.max(n.maxScore,r.score),n.fragmentError+=r.fragmentError,n.videoRanges[r.videoRange]=(n.videoRanges[r.videoRange]||0)+1,i&&i.forEach((function(t){if(t){var r=e.groups[t];r&&(n.hasDefaultAudio=n.hasDefaultAudio||e.hasDefaultAudio?r.hasDefault:r.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(r.channels).forEach((function(t){n.channels[t]=(n.channels[t]||0)+r.channels[t]})))}})),t}),{})}(v,I,e,r)),_=function(t,e,r,i,n){for(var a=Object.keys(t),s=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=s&&2===parseInt(s),u=!0,h=!1,d=1/0,c=1/0,f=1/0,g=0,v=[],m=Gr(e,n),p=m.preferHDR,E=m.allowedVideoRanges,T=function(){var e=t[a[S]];u=e.channels[2]>0,d=Math.min(d,e.minHeight),c=Math.min(c,e.minFramerate),f=Math.min(f,e.minBitrate);var r=E.filter((function(t){return e.videoRanges[t]>0}));r.length>0&&(h=!0,v=r)},S=a.length;S--;)T();d=y(d)?d:0,c=y(c)?c:0;var L=Math.max(1080,d),A=Math.max(30,c);return f=y(f)?f:r,r=Math.max(f,r),h||(e=void 0,v=[]),{codecSet:a.reduce((function(e,i){var n=t[i];if(i===e)return e;if(n.minBitrate>r)return Kr(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),e;if(!n.hasDefaultAudio)return Kr(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Kr(i,'audio codec preference "'+o+'" not found'),e;if(s&&!l){if(!n.channels[s])return Kr(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),e}else if((!o||l)&&u&&0===n.channels[2])return Kr(i,"no renditions with stereo sound found"),e;return n.minHeight>L?(Kr(i,"min resolution of "+n.minHeight+" > maximum of "+L),e):n.minFramerate>A?(Kr(i,"min framerate of "+n.minFramerate+" > maximum of "+A),e):v.some((function(t){return n.videoRanges[t]>0}))?n.maxScore=ce(e)||n.fragmentError>t[e].fragmentError)?e:(g=n.maxScore,i):(Kr(i,"no variants with VIDEO-RANGE of "+JSON.stringify(v)+" found"),e)}),void 0),videoRanges:v,preferHDR:p,minFramerate:c,minBitrate:f}}(C,R,t,k,D),x=_.codecSet,P=_.videoRanges,F=_.minFramerate,M=_.minBitrate,O=_.preferHDR;T=x,R=O?P[P.length-1]:P[0],b=F,t=Math.max(t,M),w.log("[abr] picked start tier "+JSON.stringify(_))}else T=null==S?void 0:S.codecSet,R=null==S?void 0:S.videoRange;for(var N,U=f?f.duration:c?c.duration:0,B=this.bwEstimator.getEstimateTTFB()/1e3,G=[],K=function(){var e,o=v[H],c=H>d;if(!o)return 0;if(E.useMediaCapabilities&&!o.supportedResult&&!o.supportedPromise){var g=navigator.mediaCapabilities;"function"==typeof(null==g?void 0:g.decodingInfo)&&function(t,e,r,i,n,a){var s=t.audioCodec?t.audioGroups:null,o=null==a?void 0:a.audioCodec,l=null==a?void 0:a.channels,u=l?parseInt(l):o?1/0:2,h=null;if(null!=s&&s.length)try{h=1===s.length&&s[0]?e.groups[s[0]].channels:s.reduce((function(t,r){if(r){var i=e.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(e){t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==r||t.bitrate>Math.max(n,8e6))||!!h&&y(u)&&Object.keys(h).some((function(t){return parseInt(t)>u}))}(o,I,R,b,t,k)?(o.supportedPromise=Br(o,I,g),o.supportedPromise.then((function(t){if(l.hls){o.supportedResult=t;var e=l.hls.levels,r=e.indexOf(o);t.error?w.warn('[abr] MediaCapabilities decodingInfo error: "'+t.error+'" for level '+r+" "+JSON.stringify(t)):t.supported||(w.warn("[abr] Unsupported MediaCapabilities decodingInfo result for level "+r+" "+JSON.stringify(t)),r>-1&&e.length>1&&(w.log("[abr] Removing unsupported level "+r),l.hls.removeLevel(r)))}}))):o.supportedResult=Nr}if(T&&o.codecSet!==T||R&&o.videoRange!==R||c&&b>o.frameRate||!c&&b>0&&b=2*U&&0===n?v[H].averageBitrate:v[H].maxBitrate,x=l.getTimeToLoadFrag(B,m,_*C,void 0===D);if(m>=_&&(H===h||0===o.loadError&&0===o.fragmentError)&&(x<=B||!y(x)||L&&!l.bitrateTestDelay||x"+H+" adjustedbw("+Math.round(m)+")-bitrate="+Math.round(m-_)+" ttfb:"+B.toFixed(1)+" avgDuration:"+C.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+x.toFixed(1)+" firstSelection:"+A+" codecSet:"+T+" videoRange:"+R+" hls.loadLevel:"+p)),A&&(l.firstSelection=H),{v:H}}},H=r;H>=e;H--)if(0!==(N=K())&&N)return N.v;return-1},s(t,[{key:"firstAutoLevel",get:function(){var t=this.hls,e=t.maxAutoLevel,r=t.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,e,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),e);return w.warn("[abr] Could not find best starting auto level. Defaulting to first in playlist "+s+" clamped to "+o),o}},{key:"forcedAutoLevel",get:function(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}},{key:"nextAutoLevel",get:function(){var t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;var i=e&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){var n=this.hls.levels;if(n.length>Math.max(t,i)&&n[t].loadError<=n[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(t){var e=this.hls,r=e.maxAutoLevel,i=e.minAutoLevel,n=Math.min(Math.max(t,i),r);this._nextAutoLevel!==n&&(this.nextAutoLevelKey="",this._nextAutoLevel=n)}}]),t}(),qr=function(){function t(){this._boundTick=void 0,this._tickTimer=null,this._tickInterval=null,this._tickCallCount=0,this._boundTick=this.tick.bind(this)}var e=t.prototype;return e.destroy=function(){this.onHandlerDestroying(),this.onHandlerDestroyed()},e.onHandlerDestroying=function(){this.clearNextTick(),this.clearInterval()},e.onHandlerDestroyed=function(){},e.hasInterval=function(){return!!this._tickInterval},e.hasNextTick=function(){return!!this._tickTimer},e.setInterval=function(t){return!this._tickInterval&&(this._tickCallCount=0,this._tickInterval=self.setInterval(this._boundTick,t),!0)},e.clearInterval=function(){return!!this._tickInterval&&(self.clearInterval(this._tickInterval),this._tickInterval=null,!0)},e.clearNextTick=function(){return!!this._tickTimer&&(self.clearTimeout(this._tickTimer),this._tickTimer=null,!0)},e.tick=function(){this._tickCallCount++,1===this._tickCallCount&&(this.doTick(),this._tickCallCount>1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Xr="NOT_LOADED",zr="APPENDING",Qr="PARTIAL",Jr="OK",$r=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=ti(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),Zr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=ti(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah){var c=Math.max(a,i.start(u)),f=Math.min(s,i.end(u));f>c&&(n.partial=!0,n.time.push({startPTS:c,endPTS:f}))}else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&Zr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||Zr(e))},e.getState=function(t){var e=ti(t),r=this.fragments[e];return r?r.buffered?Zr(r)?Qr:Jr:zr:Xr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=ti(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=ti(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=ti(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function Zr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function ti(t){return t.type+"_"+t.level+"_"+t.sn}var ei={length:0,start:function(){return 0},end:function(){return 0}},ri=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],E=c[y],T=257*c[m]^16843008*m;i[f]=T<<24|T>>>8,n[f]=T<<16|T>>>16,a[f]=T<<8|T>>>24,s[f]=T,T=16843009*E^65537*y^257*p^16843008*f,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,h[m]=T<<8|T>>>24,d[m]=T,f?(f=p^c[c[c[E^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&null!=a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(n){var a=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(e){r.state!==Ei&&r.state!==Ii&&(r.warn(e),r.resetFragmentLoading(t))}))},r.completeInitSegmentLoad=function(t){if(!this.levels)throw new Error("init load aborted, missing levels");var e=t.frag.stats;this.state=Ti,t.frag.data=new Uint8Array(t.payload),e.parsing.start=e.buffering.start=self.performance.now(),e.parsing.end=e.buffering.end=self.performance.now(),this.tick()},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.sn!==e.sn||t.level!==e.level},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===Fe?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?yi(ri.getBuffered(s)):"(detached)")+")"),"initSegment"!==t.sn){var o;if(t.type!==Oe){var l=t.elementaryStreams;if(!Object.keys(l).some((function(t){return!!l[t]})))return void(this.state=Ti)}var u=null==(o=this.levels)?void 0:o[t.level];null!=u&&u.fragmentError&&(this.log("Resetting level fragment error count of "+u.fragmentError+" on frag buffered"),u.fragmentError=0)}this.state=Ti,s&&(!this.loadedmetadata&&t.type==Fe&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new ii(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=Si,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===Si&&(a.state=Ti),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=Li,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),y(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=Li;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=gr(r,t.sn,o.index+1)||vr(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===A.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===bi){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===Ei||this.state===Ii||(this.state=Ti)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?gr(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return y(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=ri.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=i&&(r.maxMaxBufferLength=n,this.warn("Reduce max buffer length to "+n+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,Fe);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!y(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return Lr(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn))}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===Jr||f===Qr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(o?o.sn:"na")+" fragments: "+i),l}return n},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===A.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===wr&&d){this.resetStartWhenNotLoaded(this.levelLastLoaded);var c=Er(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+c+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+c,this.state=Ai}else if(d&&s){if(this.resetFragmentErrors(t),!(h.5;n&&this.reduceMaxBufferLength(i.len,(null==e?void 0:e.duration)||10);var a=!n;return a&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+r+" buffer"),e&&(this.fragmentTracker.removeFragment(e),this.nextLoadPosition=e.start),this.resetLoadingState(),a}return!1},r.resetFragmentErrors=function(t){t===Me&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==Ei&&(this.state=Ti)},r.afterBufferFlushed=function(t,e,r){if(t){var i=ri.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Di&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=Ti},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(!Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:hr(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1)&&null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=ki,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())},s(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(qr),xi=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:We,duration:Number.POSITIVE_INFINITY});n>>5}function Bi(t,e){return e+1=t.length)return!1;var i=Ui(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||Bi(t,n)}return!1}function Ki(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};var c=new Error("invalid ADTS sampling index:"+d);t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!0,error:c,reason:c.message})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function Hi(t){return 9216e4/t}function Vi(t,e,r,i,n){var a,s=i+n*Hi(t.samplerate),o=function(t,e){var r=Ni(t,e);if(e+r<=t.length){var i=Ui(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var Yi=null,Wi=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],ji=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],qi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Xi=[0,1,1,4];function zi(t,e,r,i,n){if(!(r+24>e.length)){var a=Qi(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Qi(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Wi[14*(3===r?3-i:3===i?3:4)+n-1],u=ji[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=qi[r][i],c=Xi[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===Yi){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Yi=v?parseInt(v[1]):0}return!!Yi&&Yi<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function Ji(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function $i(t,e){return e+18&&109===t[r+4]&&111===t[r+5]&&111===t[r+6]&&102===t[r+7])return!0;r=i>1?r+i:e}return!1}(t)},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Wt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=Ot(t,["moof"]);if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=lt(t,0,i.byteOffset-8),e.remainder=lt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=jt(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=jt(t,e),{videoTrack:e,audioTrack:Pi(),id3Track:i,textTrack:Pi()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=Ot(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==Ct(t.subarray(u,u+1));)r+=Ct(t.subarray(u,u+1)),u+=1;for(r+=Ct(t.subarray(u,u+1)),u+=1;"\0"!==Ct(t.subarray(u,u+1));)i+=Ct(t.subarray(u,u+1)),u+=1;i+=Ct(t.subarray(u,u+1)),u+=1,n=xt(t,12),a=xt(t,16),o=xt(t,20),l=xt(t,24),u=28}else if(1===e){n=xt(t,u+=4);var h=xt(t,u+=4),d=xt(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,E(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=xt(t,u),l=xt(t,u+=4),u+=4;"\0"!==Ct(t.subarray(u,u+1));)r+=Ct(t.subarray(u,u+1)),u+=1;for(r+=Ct(t.subarray(u,u+1)),u+=1;"\0"!==Ct(t.subarray(u,u+1));)i+=Ct(t.subarray(u,u+1)),u+=1;i+=Ct(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(en.test(i.schemeIdUri)){var n=y(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:qe,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),nn=function(t,e){var r=0,i=5;e+=i;for(var n=new Uint32Array(1),a=new Uint32Array(1),s=new Uint8Array(1);i>0;){s[0]=t[e];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<e.length)return-1;if(11!==e[r]||119!==e[r+1])return-1;var a=e[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&e[r+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+a];if(r+l>e.length)return-1;var u=e[r+6]>>5,h=0;2===u?h+=2:(1&u&&1!==u&&(h+=2),4&u&&(h+=2));var d=(e[r+6]<<8|e[r+7])>>12-h&1,c=[2,1,2,3,3,4,4,5][u]+d,f=e[r+5]>>3,g=7&e[r+5],v=new Uint8Array([a<<6|f<<1|g>>2,(3&g)<<6|u<<3|d<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=e.subarray(r,r+l);return t.config=v,t.channelCount=c,t.samplerate=s,t.samples.push({unit:p,pts:m}),l}var on=function(){function t(){this.VideoSample=null}var e=t.prototype;return e.createVideoSample=function(t,e,r,i){return{key:t,frame:!1,pts:e,dts:r,units:[],debug:i,length:0}},e.getLastNalUnit=function(t){var e,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.pushAccessUnit=function(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)},t}(),ln=function(){function t(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}var e=t.prototype;return e.loadWord=function(){var t=this.data,e=this.bytesAvailable,r=t.byteLength-e,i=new Uint8Array(4),n=Math.min(4,e);if(0===n)throw new Error("no bytes available");i.set(t.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},e.skipBits=function(t){var e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i4){var f=new ln(c).readSliceType();2!==f&&4!==f&&7!==f&&9!==f||(h=!0)}h&&null!=(d=l)&&d.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.frame=!0,l.key=h;break;case 5:a=!0,null!=(o=l)&&o.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.key=!0,l.frame=!0;break;case 6:a=!0,Xt(i.data,1,r.pts,e.samples);break;case 7:var g,v;a=!0,u=!0;var m=i.data,p=new ln(m).readSPS();if(!t.sps||t.width!==p.width||t.height!==p.height||(null==(g=t.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=t.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){t.width=p.width,t.height=p.height,t.pixelRatio=p.pixelRatio,t.sps=[m],t.duration=n;for(var y=m.subarray(1,4),E="avc1.",T=0;T<3;T++){var S=y[T].toString(16);S.length<2&&(S="0"+S),E+=S}t.codec=E}break;case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,l&&s.pushAccessUnit(l,t),l=s.VideoSample=s.createVideoSample(!1,r.pts,r.dts,"");break;case 12:a=!0;break;default:a=!1,l&&(l.debug+="unknown NAL "+i.type+" ")}l&&a&&l.units.push(i)})),i&&l&&(this.pushAccessUnit(l,t),this.VideoSample=null)},r.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,i),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),i>0&&(c.data=Wt(c.data,e.subarray(0,i)),c.state=0))}l=0&&a>=0){var f={data:e.subarray(u,n),type:h,state:a};o.push(f)}if(0===o.length){var g=this.getLastNalUnit(t.samples);g&&(g.data=Wt(g.data,e))}return t.naluState=a,o},e}(on),hn=function(){function t(t,e,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new pi(e,{removePKCS7Padding:!1})}var e=t.prototype;return e.decryptBuffer=function(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)},e.decryptAacSample=function(t,e,r){var i=this,n=t[e].unit;if(!(n.length<=16)){var a=n.subarray(16,n.length-n.length%16),s=a.buffer.slice(a.byteOffset,a.byteOffset+a.length);this.decryptBuffer(s).then((function(a){var s=new Uint8Array(a);n.set(s,16),i.decrypter.isSync()||i.decryptAacSamples(t,e+1,r)}))}},e.decryptAacSamples=function(t,e,r){for(;;e++){if(e>=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),dn=188,cn=function(){function t(t,e,r){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.videoParser=new un}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,e-dn)+1,i=0;i1&&(0===a&&s>2||o+dn>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:wt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._videoTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},e.demux=function(e,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._videoTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=e.length;if(this.remainderData&&(E=(e=Wt(this.remainderData,e)).length,this.remainderData=null),E>4>1){if((b=L+5+e[L+4])===L+dn)continue}else b=L+4;switch(R){case h:A&&(d&&(a=yn(d))&&this.videoParser.parseAVCPES(s,u,a,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(e.subarray(b,L+dn)),d.size+=L+dn-b);break;case c:if(A){if(g&&(a=yn(g)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a);break;case"ac3":this.parseAC3PES(o,a)}g={data:[],size:0}}g&&(g.data.push(e.subarray(b,L+dn)),g.size+=L+dn-b);break;case f:A&&(v&&(a=yn(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(b,L+dn)),v.size+=L+dn-b);break;case 0:A&&(b+=e[b]+1),y=this._pmtId=gn(e,b);break;case y:A&&(b+=e[b]+1);var k=vn(e,b,this.typeSupported,i,this.observer);(h=k.videoPid)>0&&(s.pid=h,s.segmentCodec=k.segmentVideoCodec),(c=k.audioPid)>0&&(o.pid=c,o.segmentCodec=k.segmentAudioCodec),(f=k.id3Pid)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+L+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,L=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=R}}else S++;S>0&&mn(this.observer,new Error("Found "+S+" TS packet/s that do not start with 0x47")),s.pesData=d,o.pesData=g,l.pesData=v;var D={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(D),D},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=yn(s))?(this.videoParser.parseAVCPES(i,a,e,!0,this._duration),i.pesData=null):i.pesData=s,o&&(e=yn(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e);break;case"ac3":this.parseAC3PES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=yn(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new hn(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Wt(s.sample.unit,o);else{var h=u-l;s.sample.unit.set(o.subarray(0,l),h),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else w.warn("[tsdemuxer]: AC3 PES unknown PTS")},e.parseID3PES=function(t,e){if(void 0!==e.pts){var r=o({},e,{type:this._videoTrack?qe:We,duration:Number.POSITIVE_INFINITY});t.samples.push(r)}else w.warn("[tsdemuxer]: ID3 PES unknown PTS")},t}();function fn(t,e){return((31&t[e+1])<<8)+t[e+2]}function gn(t,e){return(31&t[e+10])<<8|t[e+11]}function vn(t,e,r,i,n){var a={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},s=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0)for(var u=e+5,h=l;h>2;){106===t[u]&&(!0!==r.ac3?w.log("AC-3 audio found, not supported in this browser for now"):(a.audioPid=o,a.segmentAudioCodec="ac3"));var d=t[u+1]+2;u+=d,h-=d}break;case 194:case 135:return mn(n,new Error("Unsupported EC-3 in M2TS found")),a;case 36:return mn(n,new Error("Unsupported HEVC in M2TS found")),a}e+=l+5}return a}function mn(t,e,r){w.warn("parsing error: "+e.message),t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,levelRetry:r,error:e,reason:e.message})}function pn(t){w.log(t+" with AES-128-CBC encryption found in unencrypted stream")}function yn(t){var e,r,i,n,a,s=0,o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Wt(o[0],o[1]),o.splice(1,1);if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var l=e[7];192&l&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var u=(i=e[8])+9;if(t.size<=u)return null;t.size-=u;for(var h=new Uint8Array(t.size),d=0,c=o.length;df){u-=f;continue}e=e.subarray(u),f-=u,u=0}h.set(e,s),s+=f}return r&&(r-=i+3),{data:h,pts:n,dts:a,len:r}}return null}var En=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;var e=ct(t,0),r=(null==e?void 0:e.length)||0;if(e&&11===t[r]&&119===t[r+1]&&void 0!==vt(e)&&nn(t,r)<=16)return!1;for(var i=t.length;r1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(Sn+1)),n=Math.floor(r%(Sn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.audioStsd=function(t){var e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])},t.mp4a=function(e){return t.box(t.types.mp4a,t.audioStsd(e),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){return t.box(t.types[".mp3"],t.audioStsd(e))},t.ac3=function(e){return t.box(t.types["ac-3"],t.audioStsd(e),t.box(t.types.dac3,e.config))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):"ac3"===e.segmentCodec?t.box(t.types.stsd,t.STSD,t.ac3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(Sn+1)),o=Math.floor(i%(Sn+1));return t.box(t.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(Sn+1)),s=Math.floor(r%(Sn+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e);return Wt(t.FTYP,r)},t}();Ln.types=void 0,Ln.HDLR_TYPES=void 0,Ln.STTS=void 0,Ln.STSC=void 0,Ln.STCO=void 0,Ln.STSZ=void 0,Ln.VMHD=void 0,Ln.SMHD=void 0,Ln.STSD=void 0,Ln.FTYP=void 0,Ln.DINF=void 0;var An=9e4;function Rn(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function bn(t,e){return void 0===e&&(e=!1),Rn(t,1e3,1/An,e)}var kn=null,Dn=null,In=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===kn){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);kn=n?parseInt(n[1]):0}if(null===Dn){var a=navigator.userAgent.match(/Safari\/(\d+)/i);Dn=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,wn(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,L,A,R,b=this.videoTrackConfig;!b||e.width===b.width&&e.height===b.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=b.pixelRatio)?void 0:L[0])&&(null==(A=e.pixelRatio)?void 0:A[1])===(null==(R=b.pixelRatio)?void 0:R[1])||this.resetInitSegment()}else h=this.generateIS(t,e,n,a);var k,D=this.isVideoContiguous,I=-1;if(T&&(I=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+I+" out of "+y+" video samples due to a missing keyframe");var C=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(I),e.dropped+=I,k=v+=(e.samples[0].pts-C)/e.inputTimeScale}else-1===I&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(E&&T){var _=this.getVideoStartPts(e.samples),x=(wn(t.samples[0].pts,_)-_)/e.inputTimeScale;g+=Math.max(0,x),v+=Math.max(0,-x)}if(E){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||T||o===Me?v:void 0),T){var P=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,D,P)}}else T&&(l=this.remuxVideo(e,v,D,0));l&&(l.firstKeyFrame=I,l.independent=-1!==I,l.firstKeyFramePTS=k)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Cn(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=_n(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):Ln.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))}if(e.sps&&e.pps&&l.length){if(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:Ln.initSegment([e]),metadata:{width:e.width,height:e.height}},c)if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,wn(l[0].dts,g)-v),n=Math.min(n,g-v)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;if(!r||null===c){var y=e*s,E=l[0].pts-wn(l[0].dts,l[0].pts);kn&&null!==c&&Math.abs(y-E-c)<15e3?r=!0:c=y-E}for(var T=d.baseTime*s/d.timescale,R=0;R0?R-1:R].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var k=(a=l[l.length-1].dts)-n,D=k?Math.round(k/(h-1)):g||t.inputTimeScale/30;if(r){var I=n-c,C=I>D,_=I<-1;if((C||_)&&(C?w.warn("AVC: "+bn(I,!0)+" ms ("+I+"dts) hole between fragments detected at "+e.toFixed(3)):w.warn("AVC: "+bn(-I,!0)+" ms ("+I+"dts) overlapping between fragments detected at "+e.toFixed(3)),!_||c>=l[0].pts||kn)){n=c;var x=l[0].pts-I;if(C)l[0].dts=n,l[0].pts=x;else for(var P=0;Px);P++)l[P].dts-=I,l[P].pts-=I;w.log("Video: Initial PTS/DTS adjusted: "+bn(x,!0)+"/"+bn(n,!0)+", delta: "+bn(I,!0)+" ms")}}for(var F=0,M=0,O=n=Math.max(0,n),N=0;N0?$.dts-l[J-1].dts:D;if(st=J>0?$.pts-l[J-1].pts:D,ot.stretchShortVideoTrack&&null!==this.nextAudioPts){var ut=Math.floor(ot.maxBufferHole*s),ht=(i?v+i*s:this.nextAudioPts)-$.pts;ht>ut?((g=ht-lt)<0?g=lt:j=!0,w.log("[mp4-remuxer]: It is approximately "+ht/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=lt}else g=lt}var dt=Math.round($.pts-$.dts);q=Math.min(q,g),z=Math.max(z,g),X=Math.min(X,st),Q=Math.max(Q,st),u.push(new Pn($.key,g,tt,dt))}if(u.length)if(kn){if(kn<70){var ct=u[0].flags;ct.dependsOn=2,ct.isNonSync=0}}else if(Dn&&Q-X0&&(i&&Math.abs(p-m)<9e3||Math.abs(wn(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=wn(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var E=this.config.maxAudioFramesDrift,T=0,R=m;T=E*u&&I<1e4&&f){var C=Math.round(D/u);(R=k-C*u)<0&&(C--,R+=u),0===T&&(this.nextAudioPts=m=R),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(R/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:A.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(Ln.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new Pn(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):Ln.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=Tn.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Cn(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=Ot(e,["encv"])),i.forEach((function(t){Ot(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=Vt(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+kt.hexDump(i)+" -> "+kt.hexDump(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Ut(t);i.audio&&(e=Mn(i.audio,O)),i.video&&(r=Mn(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};y(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=Ot(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,E=p+g;!function(t,e,r){Ot(e,["moof","traf"]).forEach((function(e){Ot(e,["tfhd"]).forEach((function(i){var n=xt(i,4),a=t[n];if(a){var s=a.timescale||9e4;Ot(e,["tfdt"]).forEach((function(t){var e=t[0],i=r*s;if(i){var n=xt(t,4);if(0===e)n-=i,Mt(t,4,n=Math.max(n,0));else{n*=Math.pow(2,32),n+=xt(t,8),n-=i,n=Math.max(n,0);var a=Math.floor(n/(Dt+1)),o=Math.floor(n%(Dt+1));Mt(t,4,a),Mt(t,8,o)}}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=E:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var A={data1:d,startPTS:p,startDTS:p,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===A.type?A:void 0,h.video="audio"!==A.type?A:void 0,h.initSegment=c,h.id3=Cn(r,n,l,l),i.samples.length&&(h.text=_n(i,n,l)),h},t}();function Mn(t,e){var r=null==t?void 0:t.codec;if(r&&r.length>4)return r;if(e===O){if("ec-3"===r||"ac-3"===r||"alac"===r)return r;if("fLaC"===r||"Opus"===r)return ve(r,!1);var i="mp4a.40.5";return w.info('Parsed audio codec "'+r+'" or audio object type not handled. Using "'+i+'"'),i}return w.warn('Unhandled video codec "'+r+'"'),"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1.42e01e"}try{xn=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),xn=null==j?void 0:j.Date.now}var On=[{demux:rn,remux:Fn},{demux:cn,remux:In},{demux:tn,remux:In},{demux:En,remux:In}];On.splice(2,0,{demux:an,remux:In});var Nn=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=xn();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,R=function(t,e){var r=null;return t.byteLength>0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(R&&"AES-128"===R.method){var b=this.getDecrypter();if(!b.isSync())return this.decryptionPromise=b.webCryptoDecrypt(s,R.key.buffer,R.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var k=b.softwareDecrypt(s,R.key.buffer,R.iv.buffer);if(r.part>-1&&(k=b.flush()),!k)return a.executeEnd=xn(),Un(r);s=new Uint8Array(k)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=xn(),Un(r)}(d||c||v||D)&&this.resetInitSegment(T,m,p,E,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,R,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=xn(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=xn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=xn(),[Un(t)];var d=u.flush(o);return Bn(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=xn()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=On.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===E||0===E&&(1===T||S&&T<=0)),A=self.performance.now();(y||E||0===n.stats.parsing.start)&&(n.stats.parsing.start=A),!a||!T&&L||(a.stats.parsing.start=A);var R=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),b=new Kn(p,L,o,y,g,R);if(!L||p||R){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+R);var k=new Gn(r,i,e,s,u);this.configureTransmuxer(k)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:b},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,b);Bn(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);Bn(i)||r.async?(Bn(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,chunkMeta:e,frag:this.frag||void 0,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data;if(null!=e&&e.event){var r=this.hls;if(this.hls)switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}}else w.warn("worker message received with no "+(e?"event name":"data"))},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}();function Xn(t,e){if(t.length!==e.length)return!1;for(var r=0;r0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=Ti):(this.loadedmetadata=!1,this.state=Ri),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case Ti:this.doTickIdle();break;case Ri:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=wi}break;case Ai:var a,s=performance.now(),o=this.retryDate;if(!o||s>=o||null!=(a=this.media)&&a.seeking){var l=this.levels,u=this.trackId;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==l?void 0:l[u])||null),this.state=Ti}break;case wi:var h=this.waitingData;if(h){var d=h.frag,c=h.part,f=h.cache,g=h.complete;if(void 0!==this.initPTS[d.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=Li;var v={frag:d,part:c,payload:f.flush(),networkDetails:null};this._handleFragmentLoadProgress(v),g&&t.prototype._handleFragmentLoadComplete.call(this,v)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+d.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var m=this.getLoadPosition(),p=ri.bufferInfo(this.mediaBuffer,m,this.config.maxBufferHole);Rr(p.end,this.config.maxFragLookUpTolerance,d)<0&&(this.log("Waiting fragment cc ("+d.cc+") @ "+d.start+" cancelled because another fragment at "+p.end+" is needed"),this.clearWaitingFragment())}}else this.state=Ti}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=Ti)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if((r||!this.startFragRequested&&n.startFragPrefetch)&&null!=e&&e[i]){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==a||this.waitForCdnTuneIn(s))this.state=Ri;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,Me));var l=this.getFwdBufferInfo(o,Me);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Di);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,Fe),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len),g=s.fragments,v=g[0].start,m=this.flushing?this.getLoadPosition():l.end;if(h&&r){var p=this.getLoadPosition();u&&!zn(h.attrs,u.attrs)&&(m=p),s.PTSKnown&&pv||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=v+.05)}if(!(c>=f&&!h&&md.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var L=this.getAppendedFrag(y.start,Fe);if(null===L)return;if(E||(E=!!L.gap||!!T&&0===d.len),T&&!E||E&&l.nextStart&&l.nextStart-1)n=a[o];else{var l=Hr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(S.AUDIO_TRACKS_UPDATED,h);var d=this.trackId;if(-1!==u&&-1===d)this.setAudioTrack(u);else if(a.length&&-1===d){var c,f=new Error("No audio track selected for current audio group-ID(s): "+(null==(c=this.groupIds)?void 0:c.join(","))+" track count: "+a.length);this.warn(f.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:f})}}else this.shouldReloadPlaylist(n)&&this.setAudioTrack(this.trackId)}},r.onError=function(t,e){!e.fatal&&e.context&&(e.context.type!==xe||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))},r.setAudioOption=function(t){var e=this.hls;if(e.config.audioPreference=t,t){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&&Vr(t,i,Yr))return i;var n=Hr(t,this.tracksInGroup,Yr);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=e.loadLevel;-1===s&&(s=e.firstAutoLevel);var o=function(t,e,r,i,n){var a=e[i],s=e.reduce((function(t,e,r){var i=e.uri;return(t[i]||(t[i]=[])).push(r),t}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),h=Wr(e,i,(function(e){if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==u)return!1;var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Hr(t,a,n)>-1}));return h>-1?h:Wr(e,i,(function(e){var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Hr(t,a,n)>-1}))}(t,e.levels,r,s,Yr);if(-1===o)return null;e.nextLoadLevel=o}if(t.channels||t.audioCodec){var l=Hr(t,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(t){var e=this.tracksInGroup;if(t<0||t>=e.length)this.warn("Invalid audio track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,n=e[t],a=n.details&&!n.details.live;if(!(t===this.trackId&&n===r&&a||(this.log("Switching to audio-track "+t+' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId+" channels:"+n.channels),this.trackId=t,this.currentTrack=n,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),a))){var s=this.switchParams(n.url,null==r?void 0:r.details,n.details);this.loadPlaylist(s)}}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r),this.fragBufferedComplete(r,null)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=n.length)&&o){this.log("Subtitle track "+s+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+",duration:"+a.totalduration),this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(a.live||null!=(r=o.details)&&r.live){var u=this.mainDetails;if(a.deltaUpdateFailed||!u)return;var h,d=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details))&&d&&fr(a,l=d.start):a.hasProgramDateTime&&u.hasProgramDateTime?(li(a,u),l=a.fragments[0].start):d&&fr(a,l=d.start)}o.details=a,this.levelLastLoaded=o,s===i&&(this.startFragRequested||!this.mainDetails&&a.live||this.setStartPosition(this.mainDetails||a,l),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===Ti&&(Ar(null,a.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0)))}}else this.warn("Subtitle tracks were reset while loading level "+s)},r._handleFragmentLoadComplete=function(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=Ti}))}},r.doTick=function(){if(this.media){if(this.state===Ti){var t=this.currentTrackId,e=this.levels,r=null==e?void 0:e[t];if(!r||!e.length||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=ri.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,Fe),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=Ar(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},ea=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupIds=null,r.tracksInGroup=[],r.trackId=-1,r.currentTrack=null,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.onTextTracksChanged=function(){if(r.useTextTrackPolling||self.clearInterval(r.subtitlePollingInterval),r.media&&r.hls.config.renderTextTracksNatively){for(var t=null,e=Ye(r.media.textTracks),i=0;i-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),Ye(this.media.textTracks).forEach((function(t){He(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.groupId,n=e.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=e.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,s)}else this.warn("Subtitle track with id:"+r+" and group:"+i+" not found in active group "+(null==a?void 0:a.groupId))},r.onLevelLoading=function(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(e){var r=e.subtitleGroups||null,i=this.groupIds,n=this.currentTrack;if(!r||(null==i?void 0:i.length)!==(null==r?void 0:r.length)||null!=r&&r.some((function(t){return-1===(null==i?void 0:i.indexOf(t))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(t){return!r||-1!==r.indexOf(t.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(t){return t.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(t,e){t.id=e}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=Hr(s,a);if(o>-1)n=a[o];else{var l=Hr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,h),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}else this.shouldReloadPlaylist(n)&&this.setSubtitleTrack(this.trackId)}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=this.selectDefaultTrack,i=0;i-1){var n=this.tracksInGroup[i];return this.setSubtitleTrack(i),n}if(r)return null;var a=Hr(t,e);if(a>-1)return e[a]}}return null},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentTrack;if(this.shouldLoadPlaylist(r)&&r){var i=r.id,n=r.groupId,a=r.url;if(e)try{a=e.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Loading subtitle playlist for id "+i),this.hls.trigger(S.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:e||null})}},r.toggleTrackModes=function(){var t=this.media;if(t){var e,r=Ye(t.textTracks),i=this.currentTrack;if(i&&((e=r.filter((function(t){return Qn(i,t)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(t){"disabled"!==t.mode&&t!==e&&(t.mode="disabled")})),e){var n=this.subtitleDisplay?"showing":"hidden";e.mode!==n&&(e.mode=n)}}},r.setSubtitleTrack=function(t){var e=this.tracksInGroup;if(this.media)if(t<-1||t>=e.length||!y(t))this.warn("Invalid subtitle track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(t!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+t+(i?' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId:""));var a=i.id,s=i.groupId,o=void 0===s?"":s,l=i.name,u=i.type,h=i.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(i.url,null==r?void 0:r.details,i.details);this.loadPlaylist(d)}}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}else this.queuedDefaultTrack=t},s(e,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(t){this._subtitleDisplay=t,this.trackId>-1&&this.toggleTrackModes()}},{key:"allSubtitleTracks",get:function(){return this.tracks}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(t){this.selectDefaultTrack=!1,this.setSubtitleTrack(t)}}]),e}(Fr),ra=function(){function t(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}var e=t.prototype;return e.append=function(t,e,r){var i=this.queues[e];i.push(t),1!==i.length||r||this.executeNext(e)},e.insertAbort=function(t,e){this.queues[e].unshift(t),this.executeNext(e)},e.appendBlocker=function(t){var e,r=new Promise((function(t){e=t})),i={execute:e,onStart:function(){},onComplete:function(){},onError:function(){}};return this.append(i,t),r},e.executeNext=function(t){var e=this.queues[t];if(e.length){var r=e[0];try{r.execute()}catch(e){w.warn('[buffer-operation-queue]: Exception executing "'+t+'" SourceBuffer operation: '+e),r.onError(e);var i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}},e.shiftAndExecuteNext=function(t){this.queues[t].shift(),this.executeNext(t)},e.current=function(t){return this.queues[t][0]},t}(),ia=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,na=function(){function t(t){var e=this;this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=function(t){e.hls&&e.hls.pauseBuffering()},this._onStartStreaming=function(t){e.hls&&e.hls.resumeBuffering()},this._onMediaSourceOpen=function(){var t=e.media,r=e.mediaSource;e.log("Media source opened"),t&&(t.removeEventListener("emptied",e._onMediaEmptied),e.updateMediaElementDuration(),e.hls.trigger(S.MEDIA_ATTACHED,{media:t,mediaSource:r})),r&&r.removeEventListener("sourceopen",e._onMediaSourceOpen),e.checkPendingTracks()},this._onMediaSourceClose=function(){e.log("Media source closed")},this._onMediaSourceEnded=function(){e.log("Media source ended")},this._onMediaEmptied=function(){var t=e.mediaSrc,r=e._objectUrl;t!==r&&w.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},this.hls=t;var r,i="[buffer-controller]";this.appendSource=(r=se(t.config.preferManagedMediaSource),"undefined"!=typeof self&&r===self.ManagedMediaSource),this.log=w.log.bind(w,i),this.warn=w.warn.bind(w,i),this.error=w.error.bind(w,i),this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new ra(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media,i=se(this.appendSource);if(r&&i){var n,a=this.mediaSource=new i;this.log("created media source: "+(null==(n=a.constructor)?void 0:n.name)),a.addEventListener("sourceopen",this._onMediaSourceOpen),a.addEventListener("sourceended",this._onMediaSourceEnded),a.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(a.addEventListener("startstreaming",this._onStartStreaming),a.addEventListener("endstreaming",this._onEndStreaming));var s=this._objectUrl=self.URL.createObjectURL(a);if(this.appendSource)try{r.removeAttribute("src");var o=self.ManagedMediaSource;r.disableRemotePlayback=r.disableRemotePlayback||o&&a instanceof o,aa(r),function(t,e){var r=self.document.createElement("source");r.type="video/mp4",r.src=e,t.appendChild(r)}(r,s),r.load()}catch(t){r.src=s}else r.src=s;r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn("onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming)),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),this.mediaSrc===r?(t.removeAttribute("src"),this.appendSource&&aa(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){t.resetBuffer(e)})),this._initSourceBuffer()},e.resetBuffer=function(t){var e=this.sourceBuffer[t];try{var r;e&&(this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(r=this.mediaSource)&&r.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e))}catch(e){this.warn("onBufferReset "+t,e)}},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length,n=Object.keys(e);if(n.forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a,s=e[t],o=s.id,l=s.codec,u=s.levelCodec,h=s.container,d=s.metadata,c=me(n.codec,n.levelCodec),f=null==c?void 0:c.replace(ia,"$1"),g=me(l,u),v=null==(a=g)?void 0:a.replace(ia,"$1");if(g&&f!==v){"audio"===t.slice(0,5)&&(g=ve(g,r.appendSource));var m=h+";codecs="+g;r.appendChangeType(t,m),r.log("switching codec "+c+" to "+g),r.tracks[t]={buffer:n.buffer,codec:l,container:h,levelCodec:u,metadata:d,id:o}}}}else r.pendingTracks[t]=e[t]})),!i){var a=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==a&&(this.log(a+" bufferCodec event(s) expected "+n.join(",")),this.bufferCodecEventsExpected=a),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(r.log("changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t,!!this.pendingTracks[t])},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(r.log("Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=ri.getBuffered(e[n]);r.appendErrors[o]=0,"audio"===o||"video"===o?r.appendErrors.audiovideo=0:(r.appendErrors.audio=0,r.appendErrors.video=0),r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){var e={type:L.MEDIA_ERROR,parent:l.type,details:A.BUFFER_APPEND_ERROR,sourceBufferName:o,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=A.BUFFER_FULL_ERROR;else{var n=++r.appendErrors[o];e.details=A.BUFFER_APPEND_ERROR,r.warn("Failed "+n+"/"+i.config.appendErrorMaxRetry+' times to append segment in "'+o+'" sourceBuffer'),n>=i.config.appendErrorMaxRetry&&(e.fatal=!0)}i.trigger(S.ERROR,e)}};n.append(y,o,!!this.pendingTracks[o])},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){r.warn("Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&a.push("video")),0===a.length&&this.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.trimBuffers()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,r.log(i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(r.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&r.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.trimBuffers=function(){var t=this.hls,e=this.details,r=this.media;if(r&&null!==e&&this.getSourceBufferTypes().length){var i=t.config,n=r.currentTime,a=e.levelTargetDuration,s=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(y(s)&&s>0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}if(y(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){var u=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),h=Math.max(u,a),d=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,d)}}},e.flushBackBuffer=function(t,e,r){var i=this,n=this.details,a=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(s){var o=a[s];if(o){var l=ri.getBuffered(o);if(l.length>0&&r>l.start(0)){if(i.hls.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:r}),null!=n&&n.live)i.hls.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(o.ended&&l.end(l.length-1)-t<2*e)return void i.log("Cannot flush "+s+" back buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:s})}}}))},e.flushFrontBuffer=function(t,e,r){var i=this,n=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(a){var s=n[a];if(s){var o=ri.getBuffered(s),l=o.length;if(l<2)return;var u=o.start(l-1),h=o.end(l-1);if(r>u||t>=u&&t<=h)return;if(s.ended&&t-h<2*e)return void i.log("Cannot flush "+a+" front buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:u,endOffset:1/0,type:a})}}))},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=y(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!y(a))&&(this.log("Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);this.log("Media Source duration is set to "+e.duration+". Setting seekable range to "+i+"-"+n+"."),e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&(!t||2===i||"audiovideo"in r)){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this,r=this.sourceBuffer,i=this.mediaSource;if(!i)throw Error("createSourceBuffers called when mediaSource was null");var n=function(n){if(!r[n]){var a,s=t[n];if(!s)throw Error("source buffer exists for track "+n+", however track does not");var o=-1===(null==(a=s.levelCodec)?void 0:a.indexOf(","))?s.levelCodec:s.codec;o&&"audio"===n.slice(0,5)&&(o=ve(o,e.appendSource));var l=s.container+";codecs="+o;e.log("creating sourceBuffer("+l+")");try{var u=r[n]=i.addSourceBuffer(l),h=n;e.addBufferListener(h,"updatestart",e._onSBUpdateStart),e.addBufferListener(h,"updateend",e._onSBUpdateEnd),e.addBufferListener(h,"error",e._onSBUpdateError),e.appendSource&&e.addBufferListener(h,"bufferedchange",(function(t,r){var i=r.removedRanges;null!=i&&i.length&&e.hls.trigger(S.BUFFER_FLUSHED,{type:n})})),e.tracks[n]={buffer:u,codec:o,container:s.container,levelCodec:s.levelCodec,metadata:s.metadata,id:s.id}}catch(t){e.error("error while trying to add sourceBuffer: "+t.message),e.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:n,mimeType:l})}}};for(var a in t)n(a)},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e;if("closed"!==(null==(e=this.mediaSource)?void 0:e.readyState)){var r=this.operationQueue;r.current(t).onComplete(),r.shiftAndExecuteNext(t)}else this.resetBuffer(t)},e._onSBUpdateError=function(t,e){var r,i=new Error(t+" SourceBuffer error. MediaSource readyState: "+(null==(r=this.mediaSource)?void 0:r.readyState));this.error(""+i,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});var n=this.operationQueue.current(t);n&&n.onError(i)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return this.warn("Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=y(i.duration)?i.duration:1/0,l=y(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&(!s.ending||s.ended)?(s.ended=!1,this.log("Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.sourceBuffer[e];if(r)r.ended=!1,r.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error("Attempting to append to the "+e+" SourceBuffer, but it does not exist")},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},s(t,[{key:"mediaSrc",get:function(){var t,e,r=(null==(t=this.media)||null==(e=t.querySelector)?void 0:e.call(t,"source"))||this.media;return null==r?void 0:r.src}}]),t}();function aa(t){var e=t.querySelectorAll("source");[].slice.call(e).forEach((function(e){t.removeChild(e)}))}var sa={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},oa=function(t){return String.fromCharCode(sa[t]||t)},la=15,ua=100,ha={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},da={17:2,18:4,21:6,22:8,23:10,19:13,20:15},ca={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},fa={25:2,26:4,29:6,30:8,31:10,27:13,28:15},ga=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],va=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),ma=function(t){for(var e=[],r=0;rua&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=ua)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=oa(t);this.pos>=ua?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),Sa=function(){function t(t,e,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new Ta(r),this.nonDisplayedMemory=new Ta(r),this.lastOutputScreen=new Ta(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),La=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new va;this.channels=[null,new Sa(t,e,i),new Sa(t+1,r,i)]}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r=this;this.logger.time=t;for(var i=function(t){var i=127&e[t],n=127&e[t+1],a=!1,s=null;if(0===i&&0===n)return 0;r.logger.log(3,(function(){return"["+ma([e[t],e[t+1]])+"] -> ("+ma([i,n])+")"}));var o=r.cmdHistory;if(i>=16&&i<=31){if(function(t,e,r){return r.a===t&&r.b===e}(i,n,o))return Aa(null,null,o),r.logger.log(3,(function(){return"Repeated command ("+ma([i,n])+") is dropped"})),0;Aa(i,n,r.cmdHistory),(a=r.parseCmd(i,n))||(a=r.parseMidrow(i,n)),a||(a=r.parsePAC(i,n)),a||(a=r.parseBackgroundAttributes(i,n))}else Aa(null,null,o);if(!a&&(s=r.parseChars(i,n))){var l=r.currentChannel;l&&l>0?r.channels[l].insertChars(s):r.logger.log(2,"No channel found yet. TEXT-MODE?")}a||s||r.logger.log(2,(function(){return"Couldn't parse cleaned data "+ma([i,n])+" orig: "+ma([e[t],e[t+1]])}))},n=0;n=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;var r=20===t||21===t||23===t?1:2,i=this.channels[r];return 20===t||21===t||28===t||29===t?32===e?i.ccRCL():33===e?i.ccBS():34===e?i.ccAOF():35===e?i.ccAON():36===e?i.ccDER():37===e?i.ccRU(2):38===e?i.ccRU(3):39===e?i.ccRU(4):40===e?i.ccFON():41===e?i.ccRDC():42===e?i.ccTR():43===e?i.ccRTD():44===e?i.ccEDM():45===e?i.ccCR():46===e?i.ccENM():47===e&&i.ccEOC():i.ccTO(e-32),this.currentChannel=r,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,(function(){return"MIDROW ("+ma([t,e])+")"})),!0)}return!1},e.parsePAC=function(t,e){var r;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;var i=t<=23?1:2;r=e>=64&&e<=95?1===i?ha[t]:ca[t]:1===i?da[t]:fa[t];var n=this.channels[i];return!!n&&(n.setPAC(this.interpretPAC(r,e)),this.currentChannel=i,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},e.parseChars=function(t,e){var r,i,n=null,a=null;return t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,(function(){return"Special char '"+oa(i)+"' in channel "+r})),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n&&this.logger.log(3,(function(){return"Char codes = "+ma(n).join(",")})),n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=ga[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),ba=function(){if(null!=j&&j.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),ka=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function Da(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Ia=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function wa(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var Ca=new ba(0,0,""),_a="middle"===Ca.align?"middle":"center";function xa(t,e,r){var i=t;function n(){var e=Da(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Ia;wa(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",_a,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",_a,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",_a,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===Ca.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",_a);var a=i.get("position","auto");"auto"===a&&50===Ca.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function Pa(t){return t.replace(//gi,"\n")}var Fa=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new ka,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=Pa(t);r>>0).toString()};function Ua(t,e,r){return Na(t.toString())+Na(e.toString())+Na(r)}function Ba(t,e,r,i,n,a,s){var o,l,u,h=new Fa,d=Rt(new Uint8Array(t)).trim().replace(Ma,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),Rn(o,An,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=wn(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=Ua(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(Oa(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){Oa(t,"LOCAL:")?g=t.slice(6):Oa(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(y(e)&&y(r)&&y(i)&&y(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var Ga="stpp.ttml.im1t",Ka=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Ha=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Va={left:"start",center:"center",right:"end",start:"start",end:"end"};function Ya(t,e,r,i){var n=Ot(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return Rt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),Rn(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},a=Object.keys(n).reduce((function(t,e){return t[e]=i.getAttribute("ttp:"+e)||n[e],t}),{}),s="preserve"!==i.getAttribute("xml:space"),l=ja(Wa(i,"styling","style")),u=ja(Wa(i,"layout","region")),h=Wa(i,"body","[begin]");return[].map.call(h,(function(t){var r=qa(t,s);if(!r||!t.hasAttribute("begin"))return null;var i=Qa(t.getAttribute("begin"),a),n=Qa(t.getAttribute("dur"),a),h=Qa(t.getAttribute("end"),a);if(null===i)throw za(t);if(null===h){if(null===n)throw za(t);h=i+n}var d=new ba(i-e,h-e,r);d.id=Ua(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=Xa(e,i,a)||Xa(t,i,a)||Xa(n,i,a);return s&&(r[a]=s),r}),{})}(u[t.getAttribute("region")],l[t.getAttribute("style")],l),f=c.textAlign;if(f){var g=Va[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function Wa(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function ja(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function qa(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?qa(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function Xa(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function za(t){return new Error("Could not parse ttml timestamp "+t)}function Qa(t,e){if(!t)return null;var r=Da(t);return null===r&&(Ka.test(t)?r=function(t,e){var r=Ka.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):Ha.test(t)&&(r=function(t,e){var r=Ha.exec(t),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var Ja=function(){function t(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0},e.initCea608Parsers=function(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){var t=new Ra(this,"textTrack1"),e=new Ra(this,"textTrack2"),r=new Ra(this,"textTrack3"),i=new Ra(this,"textTrack4");this.cea608Parser1=new La(1,t,e),this.cea608Parser2=new La(3,r,i)}},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t,e){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||Ya(e,this.initPTS[t.cc],(function(){i.textCodec=Ga,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Ke(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===Oe&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){this.initCea608Parsers();var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==Fe||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;sthis.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=Math.max(e,r),o=0;o=s||l.height>=s)&&(i=l,!(n=t[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},s(t,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),es=function(){function t(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),rs="[eme]",is=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,rs),this.log=w.log.bind(w,rs),this.warn=w.warn.bind(w,rs),this.error=w.error.bind(w,rs),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===q.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof ls?e:new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===ot&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case q.FAIRPLAY:n=["cenc","sinf"];break;case q.WIDEVINE:case q.PLAYREADY:n=["cenc"];break;case q.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+kt.hexDump(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return kt.hexDump(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+kt.hexDump((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=at(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=nt(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof ls?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:at(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=at(this.config)),0===t.length)throw new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData,n='"'+t.type+'" event: init data type: "'+r+'"';if(this.debug(n),null!==i){var a,s;if("sinf"===r&&this.config.drmSystems[q.FAIRPLAY]){var o=Ct(new Uint8Array(i));try{var l=V(JSON.parse(o).sinf),u=Vt(new Uint8Array(l));if(!u)throw new Error("'schm' box missing or not cbcs/cenc with schi > tenc");a=u.subarray(8,24),s=q.FAIRPLAY}catch(t){return void this.warn(n+" Failed to parse sinf: "+t)}}else{var h=function(t){var e=[];if(t instanceof ArrayBuffer)for(var r=t.byteLength,i=0;i+320)for(var a,s=0,o=n.length;s in key message");return W(atob(f))},e.setupLicenseXHR=function(t,e,r,i){var n=this,a=this.config.licenseXhrSetup;return a?Promise.resolve().then((function(){if(!r.decryptdata)throw new Error("Key removed");return a.call(n.hls,t,e,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return t.open("POST",e,!0),a.call(n.hls,t,e,r,i)})).then((function(r){return t.readyState||t.open("POST",e,!0),{xhr:t,licenseChallenge:r||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))},e.requestLicense=function(t,e){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrl(t.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!t.mediaKeysSession)return a(new Error("invalid state"));if(4===o.readyState)if(200===o.status){r._requestLicenseFailureCount=0;var l=o.response;r.log("License received "+(l instanceof ArrayBuffer?l.byteLength:l));var u=r.config.licenseResponseCallback;if(u)try{l=u.call(r.hls,o,s,t)}catch(t){r.error(t)}n(l)}else{var h=i.errorRetry,d=h?h.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>d||o.status>=400&&o.status<500)a(new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(e){var i=e.xhr,n=e.licenseChallenge;t.keySystem==q.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},Zt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t)})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t)}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),t._onmessage&&(r.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();is.CDMCleanupPromise=void 0;var ns,as,ss,os,ls=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(c(Error));!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(ns||(ns={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}(as||(as={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}(ss||(ss={}));var us=((os={})[ss.OBJECT]=["br","d","ot","tb"],os[ss.REQUEST]=["bl","dl","mtp","nor","nrr","su"],os[ss.SESSION]=["cid","pr","sf","sid","st","v"],os[ss.STATUS]=["bs","rtp"],os),hs=function t(e,r){this.value=void 0,this.params=void 0,Array.isArray(e)&&(e=e.map((function(e){return e instanceof t?e:new t(e)}))),this.value=e,this.params=r},ds=function(t){this.description=void 0,this.description=t},cs="Dict";function fs(t,e,r,i){return new Error("failed to "+t+' "'+(n=e,(Array.isArray(n)?JSON.stringify(n):n instanceof Map?"Map{}":n instanceof Set?"Set{}":"object"==typeof n?JSON.stringify(n):String(n))+'" as ')+r,{cause:i});var n}var gs="Bare Item",vs="Boolean",ms="Byte Sequence",ps="Decimal",ys="Integer",Es=/[\x00-\x1f\x7f]+/,Ts="Token",Ss="Key";function Ls(t,e,r){return fs("serialize",t,e,r)}function As(t){if(!1===ArrayBuffer.isView(t))throw Ls(t,ms);return":"+(e=t,btoa(String.fromCharCode.apply(String,e))+":");var e}function Rs(t){if(function(t){return t<-999999999999999||99999999999999912)throw Ls(t,ps);var r=e.toString();return r.includes(".")?r:r+".0"}var Ds="String";function Is(t){var e,r=(e=t).description||e.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw Ls(r,Ts);return r}function ws(t){switch(typeof t){case"number":if(!y(t))throw Ls(t,gs);return Number.isInteger(t)?Rs(t):ks(t);case"string":return function(t){if(Es.test(t))throw Ls(t,Ds);return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(t);case"symbol":return Is(t);case"boolean":return function(t){if("boolean"!=typeof t)throw Ls(t,vs);return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return"@"+Rs(t.getTime()/1e3)}(t);if(t instanceof Uint8Array)return As(t);if(t instanceof ds)return Is(t);default:throw Ls(t,gs)}}function Cs(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw Ls(t,Ss);return t}function _s(t){return null==t?"":Object.entries(t).map((function(t){var e=t[0],r=t[1];return!0===r?";"+Cs(e):";"+Cs(e)+"="+ws(r)})).join("")}function xs(t){return t instanceof hs?""+ws(t.value)+_s(t.params):ws(t)}function Ps(t,e){var r;if(void 0===e&&(e={whitespace:!0}),"object"!=typeof t)throw Ls(t,cs);var i=t instanceof Map?t.entries():Object.entries(t),n=null!=(r=e)&&r.whitespace?" ":"";return Array.from(i).map((function(t){var e=t[0],r=t[1];r instanceof hs==0&&(r=new hs(r));var i,n=Cs(e);return!0===r.value?n+=_s(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(xs).join(" ")+")"+_s(i.params):n+=xs(r)),n})).join(","+n)}var Fs=function(t){return"ot"===t||"sf"===t||"st"===t},Ms=function(t){return"number"==typeof t?y(t):null!=t&&""!==t&&!1!==t},Os=function(t){return Math.round(t)},Ns=function(t){return 100*Os(t/100)},Us={br:Os,d:Os,bl:Ns,dl:Ns,mtp:Ns,nor:function(t,e){return null!=e&&e.baseUrl&&(t=function(t,e){var r=new URL(t),i=new URL(e);if(r.origin!==i.origin)return t;for(var n=r.pathname.split("/").slice(1),a=i.pathname.split("/").slice(1,-1);n[0]===a[0];)n.shift(),a.shift();for(;a.length;)a.shift(),n.unshift("..");return n.join("/")}(t,e.baseUrl)),encodeURIComponent(t)},rtp:Ns,tb:Os};function Bs(t,e){return void 0===e&&(e={}),t?function(t,e){return Ps(t,e)}(function(t,e){var r={};if(null==t||"object"!=typeof t)return r;var i=Object.keys(t).sort(),n=o({},Us,null==e?void 0:e.formatters),a=null==e?void 0:e.filter;return i.forEach((function(i){if(null==a||!a(i)){var s=t[i],o=n[i];o&&(s=o(s,e)),"v"===i&&1===s||"pr"==i&&1===s||Ms(s)&&(Fs(i)&&"string"==typeof s&&(s=new ds(s)),r[i]=s)}})),r}(t,e),o({whitespace:!1},e)):""}function Gs(t,e,r){return o(t,function(t,e){var r;if(void 0===e&&(e={}),!t)return{};var i=Object.entries(t),n=Object.entries(us).concat(Object.entries((null==(r=e)?void 0:r.customHeaderMap)||{})),a=i.reduce((function(t,e){var r,i=e[0],a=e[1],s=(null==(r=n.find((function(t){return t[1].includes(i)})))?void 0:r[0])||ss.REQUEST;return null!=t[s]||(t[s]={}),t[s][i]=a,t}),{});return Object.entries(a).reduce((function(t,r){var i=r[0],n=r[1];return t[i]=Bs(n,e),t}),{})}(e,r))}var Ks="CMCD",Hs=/CMCD=[^&#]+/;function Vs(t,e,r){var i=function(t,e){if(void 0===e&&(e={}),!t)return"";var r=Bs(t,e);return Ks+"="+encodeURIComponent(r)}(e,r);if(!i)return t;if(Hs.test(t))return t.replace(Hs,i);var n=t.includes("?")?"&":"?";return""+t+n+i}var Ys=function(){function t(t){var e=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.includeKeys=void 0,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){e.initialized&&(e.starved=!0),e.buffering=!0},this.onPlaying=function(){e.initialized||(e.initialized=!0),e.buffering=!1},this.applyPlaylistData=function(t){try{e.apply(t,{ot:ns.MANIFEST,su:!e.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var r=t.frag,i=e.hls.levels[r.level],n=e.getObjectType(r),a={d:1e3*r.duration,ot:n};n!==ns.VIDEO&&n!==ns.AUDIO&&n!=ns.MUXED||(a.br=i.bitrate/1e3,a.tb=e.getTopBandwidth(n)/1e3,a.bl=e.getBufferLength(n)),e.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=t;var r=this.config=t.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||function(){try{return crypto.randomUUID()}catch(i){try{var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var e=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==t?e:3&e|8).toString(16)}))}}}(),this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:as.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(t,e){void 0===e&&(e={}),o(e,this.createData());var r=e.ot===ns.INIT||e.ot===ns.VIDEO||e.ot===ns.MUXED;this.starved&&r&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);var i=this.includeKeys;i&&(e=Object.keys(e).reduce((function(t,r){return i.includes(r)&&(t[r]=e[r]),t}),{})),this.useHeaders?(t.headers||(t.headers={}),Gs(t.headers,e)):t.url=Vs(t.url,e)},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?ns.TIMED_TEXT:"initSegment"===t.sn?ns.INIT:"audio"===e?ns.AUDIO:"main"===e?this.hls.audioTracks.length?ns.VIDEO:ns.MUXED:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===ns.AUDIO)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=g(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===ns.AUDIO?this.audioBuffer:this.videoBuffer;return r&&e?1e3*ri.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t}(),Ws=function(){function t(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=w.log.bind(w,"[content-steering]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.ERROR,this.onError,this))},e.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}},e.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},e.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},e.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},e.removeLevel=function(t){var e=this.levels;e&&(this.levels=e.filter((function(e){return e!==t})))},e.onManifestLoading=function(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null},e.onManifestLoaded=function(t,e){var r=e.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},e.onManifestParsed=function(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks},e.onError=function(t,e){var r=e.errorAction;if((null==r?void 0:r.action)===Dr&&r.flags===_r){var i=this.levels,n=this.pathwayPriority,a=this.pathwayId;if(e.context){var s=e.context,o=s.groupId,l=s.pathwayId,u=s.type;o&&i?a=this.getPathwayForGroupId(o,u,a):l&&(a=l)}a in this.penalizedPathways||(this.penalizedPathways[a]=performance.now()),!n&&i&&(n=i.reduce((function(t,e){return-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t}),[])),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),r.resolved||w.warn("Could not resolve "+e.details+' ("'+e.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+JSON.stringify(n)+" penalized: "+JSON.stringify(this.penalizedPathways))}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,mr(e),this.hls.trigger(S.LEVELS_UPDATED,{levels:e});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},e.getPathwayForGroupId=function(t,e,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;Sr(d,r.retry,!1,{url:t.url,data:void 0,code:a})?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){if(this.config){var t=this.config.loadPolicy.timeoutRetry;if(Sr(t,this.stats.retry,!0))this.retry(t);else{var e;w.warn("timeout while loading "+(null==(e=this.context)?void 0:e.url));var r=this.callbacks;r&&(this.abortInternal(),r.onTimeout(this.stats,this.context,this.loader))}}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=Er(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==e?void 0:e.url)+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Xs.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Qs=/(\d+)-(\d+)\/(\d+)/,Js=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||$s,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null},e.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&y(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new to(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Qs.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(y(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&y(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var o=i.response;if(!o)throw new Error("loader destroyed");self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!y(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new xi,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function $s(t,e){return new self.Request(t.url,e)}var Zs,to=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(c(Error)),eo=/\s/,ro=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:zs,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:jr,bufferController:na,capLevelController:ts,errorController:Pr,fpsController:es,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:ot,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=Pa(l.trim()),v=Ua(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Ke(t,e)}))),u}},enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:Zn,subtitleTrackController:ea,timelineController:Ja,audioStreamController:Jn,audioTrackController:$n,emeController:is,cmcdController:Ys,contentSteeringController:Ws});function io(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(io):Object.keys(t).reduce((function(e,r){return e[r]=io(t[r]),e}),{}):t}function no(t){var e=t.loader;e!==Js&&e!==zs?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=Js,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var ao=function(t){function e(e,r){var i;return(i=t.call(this,e,"[level-controller]")||this)._levels=[],i._firstLevel=-1,i._maxAutoLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.prototype.stopLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1},r.onManifestLoading=function(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r=this.hls.config.preferManagedMediaSource,i=[],n={},a={},s=!1,o=!1,l=!1;e.levels.forEach((function(t){var e,u,h=t.attrs,d=t.audioCodec,c=t.videoCodec;-1!==(null==(e=d)?void 0:e.indexOf("mp4a.40.34"))&&(Zs||(Zs=/chrome|firefox/i.test(navigator.userAgent)),Zs&&(t.audioCodec=d=void 0)),d&&(t.audioCodec=d=ve(d,r)),0===(null==(u=c)?void 0:u.indexOf("avc1"))&&(c=t.videoCodec=function(t){for(var e=t.split(","),r=0;r2){var n=i.shift()+".";n+=parseInt(i.shift()).toString(16),n+=("000"+parseInt(i.shift()).toString(16)).slice(-4),e[r]=n}}return e.join(",")}(c));var f=t.width,g=t.height,v=t.unknownCodecs;if(s||(s=!(!f||!g)),o||(o=!!c),l||(l=!!d),!(null!=v&&v.length||d&&!le(d,"audio",r)||c&&!le(c,"video",r))){var m=h.CODECS,p=h["FRAME-RATE"],y=h["HDCP-LEVEL"],E=h["PATHWAY-ID"],T=h.RESOLUTION,S=h["VIDEO-RANGE"],L=(E||".")+"-"+t.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(n[L])if(n[L].uri===t.url||t.attrs["PATHWAY-ID"])n[L].addGroupId("audio",h.AUDIO),n[L].addGroupId("text",h.SUBTITLES);else{var A=a[L]+=1;t.attrs["PATHWAY-ID"]=new Array(A+1).join(".");var R=new or(t);n[L]=R,i.push(R)}else{var b=new or(t);n[L]=b,a[L]=1,i.push(b)}}})),this.filterAndSortMediaOptions(i,e,s,o,l)},r.filterAndSortMediaOptions=function(t,e,r,i,n){var a=this,s=[],o=[],l=t;if((r||i)&&n&&(l=l.filter((function(t){var e,r=t.videoCodec,i=t.videoRange,n=t.width,a=t.height;return(!!r||!(!n||!a))&&!!(e=i)&&er.indexOf(e)>-1}))),0!==l.length){if(e.audioTracks){var u=this.hls.config.preferManagedMediaSource;so(s=e.audioTracks.filter((function(t){return!t.audioCodec||le(t.audioCodec,"audio",u)})))}e.subtitles&&so(o=e.subtitles);var h=l.slice(0);l.sort((function(t,e){if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return er.indexOf(t.videoRange)-er.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){var i=de(t.videoCodec),n=de(e.videoCodec);if(i!==n)return n-i}if(t.uri===e.uri&&t.codecSet!==e.codecSet){var a=ce(t.codecSet),s=ce(e.codecSet);if(a!==s)return s-a}return t.averageBitrate!==e.averageBitrate?t.averageBitrate-e.averageBitrate:0}));var d=h[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==h.length)for(var c=0;cm&&m===ro.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=p)}break}var y=n&&!i,E={levels:l,audioTracks:s,subtitleTracks:o,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:n,video:i,altAudio:!y&&s.some((function(t){return!!t.url}))};this.hls.trigger(S.MANIFEST_PARSED,E),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}else Promise.resolve().then((function(){if(a.hls){e.levels.length&&a.warn("One or more CODECS in variant not supported: "+JSON.stringify(e.levels[0].attrs));var t=new Error("no level with compatible codecs found in manifest");a.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}))},r.onError=function(t,e){!e.fatal&&e.context&&e.context.type===_e&&e.context.level===this.level&&this.checkRetry(e)},r.onFragBuffered=function(t,e){var r=e.frag;if(void 0!==r&&r.type===Fe){var i=r.elementaryStreams;if(!Object.keys(i).some((function(t){return!!i[t]})))return;var n=this._levels[r.level];null!=n&&n.loadError&&(this.log("Resetting level error count of "+n.loadError+" on frag buffered"),n.loadError=0)}},r.onLevelLoaded=function(t,e){var r,i,n=e.level,a=e.details,s=this._levels[n];if(!s)return this.warn("Invalid level index "+n),void(null!=(i=e.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));n===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0),this.playlistLoaded(n,e,s.details)):null!=(r=e.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentLevelIndex,i=this.currentLevel;if(i&&this.shouldLoadPlaylist(i)){var n=i.uri;if(e)try{n=e.addDirectives(n)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}var a=i.attrs["PATHWAY-ID"];this.log("Loading level index "+r+(void 0!==(null==e?void 0:e.msn)?" at sn "+e.msn+" part "+e.part:"")+" with"+(a?" Pathway "+a:"")+" "+n),this.clearTimer(),this.hls.trigger(S.LEVEL_LOADING,{url:n,level:r,pathwayId:i.attrs["PATHWAY-ID"],id:0,deliveryDirectives:e||null})}},r.removeLevel=function(t){var e,r=this,i=this._levels.filter((function(e,i){return i!==t||(r.steering&&r.steering.removeLevel(e),e===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((function(t){return t.level=-1}))),!1)}));mr(i),this._levels=i,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(S.LEVELS_UPDATED,{levels:i})},r.onLevelsUpdated=function(t,e){var r=e.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var t=this.hls,e=t.autoLevelCapping,r=t.maxAutoLevel,i=t.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(S.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:e,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},s(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=e[t],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=o,n!==t||!o.details||!a||s!==l){this.log("Switching to level "+t+" ("+(o.height?o.height+"p ":"")+(o.videoRange?o.videoRange+" ":"")+(o.codecSet?o.codecSet+" ":"")+"@"+o.bitrate+")"+(l?" with Pathway "+l:"")+" from level "+n+(s?" with Pathway "+s:""));var u={level:t,attrs:o.attrs,details:o.details,bitrate:o.bitrate,averageBitrate:o.averageBitrate,maxBitrate:o.maxBitrate,realBitrate:o.realBitrate,width:o.width,height:o.height,codecSet:o.codecSet,audioCodec:o.audioCodec,videoCodec:o.videoCodec,audioGroups:o.audioGroups,subtitleGroups:o.subtitleGroups,loaded:o.loaded,loadError:o.loadError,fragmentError:o.fragmentError,name:o.name,id:o.id,uri:o.uri,url:o.url,urlId:0,audioGroupIds:o.audioGroupIds,textGroupIds:o.textGroupIds};this.hls.trigger(S.LEVEL_SWITCHING,u);var h=o.details;if(!h||h.live){var d=this.switchParams(o.uri,null==a?void 0:a.details,h);this.loadPlaylist(d)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(Fr);function so(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var oo=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){var i;if(t&&t!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=A.KEY_LOAD_ERROR),new fi({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i2,c=!h||e&&e.start<=a||h-a>2&&!this.fragmentTracker.getPartialFragment(a);if(d||c)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var f;if(!(u.len>0||h))return;var g=Math.max(h,u.start||0)-a,v=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,m=(null==v||null==(f=v.details)?void 0:f.live)?2*v.details.targetduration:2,p=this.fragmentTracker.getPartialFragment(a);if(g>0&&(g<=m||p))return void(i.paused||this._trySkipBufferHole(p))}var y=self.performance.now();if(null!==n){var E=y-n;if(s||!(E>=250)||(this._reportStall(u),this.media)){var T=ri.bufferInfo(i,a,r.maxBufferHole);this._tryFixBufferStall(T,E)}}else this.stalled=y}else if(this.moved=!0,s||(this.nudgeRetry=0),null!==n){if(this.stallReported){var S=self.performance.now()-n;w.warn("playback not stuck anymore @"+a+", after "+Math.round(S)+"ms"),this.stallReported=!1}this.stalled=null}}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,n=this.media;if(null!==n){var a=n.currentTime,s=i.getPartialFragment(a);if(s&&(this._trySkipBufferHole(s)||!this.media))return;(t.len>r.maxBufferHole||t.nextStart&&t.nextStart-a1e3*r.highBufferWatchdogPeriod&&(w.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}},e._reportStall=function(t){var e=this.hls,r=this.media;if(!this.stallReported&&r){this.stallReported=!0;var i=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+JSON.stringify(t)+")");w.warn(i.message),e.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}},e._trySkipBufferHole=function(t){var e=this.config,r=this.hls,i=this.media;if(null===i)return 0;var n=i.currentTime,a=ri.bufferInfo(i,n,0),s=n0&&a.len<1&&i.readyState<3,u=s-n;if(u>0&&(o||l)){if(u>e.maxBufferHole){var h=this.fragmentTracker,d=!1;if(0===n){var c=h.getAppendedFrag(0,Fe);c&&s1?(i=0,this.bitrateTest=!0):i=r.firstAutoLevel),r.nextLoadLevel=i,this.level=r.loadLevel,this.loadedmetadata=!1}e>0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=Ti,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=Ei},r.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case Ci:var t=this.levels,e=this.level,r=null==t?void 0:t[e],i=null==r?void 0:r.details;if(i&&(!i.live||this.levelLastLoaded===r)){if(this.waitForCdnTuneIn(i))break;this.state=Ti;break}if(this.hls.nextLoadLevel!==this.level){this.state=Ti;break}break;case Ai:var n,a=self.performance.now(),s=this.retryDate;if(!s||a>=s||null!=(n=this.media)&&n.seeking){var o=this.levels,l=this.level,u=null==o?void 0:o[l];this.resetStartWhenNotLoaded(u||null),this.state=Ti}}this.state===Ti&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},r.doTickIdle=function(){var t=this.hls,e=this.levelLastLoaded,r=this.levels,i=this.media;if(null!==e&&(i||!this.startFragRequested&&t.config.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)){var n=t.nextLoadLevel;if(null!=r&&r[n]){var a=r[n],s=this.getMainFwdBufferInfo();if(null!==s){var o=this.getLevelDetails();if(o&&this._streamEnded(s,o)){var l={};return this.altAudio&&(l.type="video"),this.hls.trigger(S.BUFFER_EOS,l),void(this.state=Di)}t.loadLevel!==n&&-1===t.manualLevel&&this.log("Adapting to level "+n+" from level "+this.level),this.level=t.nextLoadLevel=n;var u=a.details;if(!u||this.state===Ci||u.live&&this.levelLastLoaded!==a)return this.level=n,void(this.state=Ci);var h=s.len,d=this.getMaxBufferLength(a.maxBitrate);if(!(h>=d)){this.backtrackFragment&&this.backtrackFragment.start>s.end&&(this.backtrackFragment=null);var c=this.backtrackFragment?this.backtrackFragment.start:s.end,f=this.getNextFragment(c,u);if(this.couldBacktrack&&!this.fragPrevious&&f&&"initSegment"!==f.sn&&this.fragmentTracker.getState(f)!==Jr){var g,v=(null!=(g=this.backtrackFragment)?g:f).sn-u.startSN,m=u.fragments[v-1];m&&f.cc===m.cc&&(f=m,this.fragmentTracker.removeFragment(m))}else this.backtrackFragment&&s.len&&(this.backtrackFragment=null);if(f&&this.isLoopLoading(f,c)){if(!f.gap){var p=this.audioOnly&&!this.altAudio?O:N,y=(p===N?this.videoBuffer:this.mediaBuffer)||this.media;y&&this.afterBufferFlushed(y,p,Fe)}f=this.getNextFragmentLoopLoading(f,u,s,Fe,d)}f&&(!f.initSegment||f.initSegment.data||this.bitrateTest||(f=f.initSegment),this.loadFragment(f,a,c))}}}}},r.loadFragment=function(e,r,i){var n=this.fragmentTracker.getState(e);this.fragCurrent=e,n===Xr||n===Qr?"initSegment"===e.sn?this._loadInitSegment(e,r):this.bitrateTest?(this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e,r)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):this.clearTrackerIfNeeded(e)},r.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,Fe)},r.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);var n=this.getLevelDetails();if(null!=n&&n.live){var a=this.getMainFwdBufferInfo();if(!a||a.len<2*n.targetduration)return}if(!e.paused&&t){var s=t[this.hls.nextLoadLevel],o=this.fragLastKbps;r=o&&this.fragCurrent?this.fragCurrent.duration*s.maxBitrate/(1e3*o)+1:0}else r=0;var l=this.getBufferedFrag(e.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var h=u.maxStartPTS?u.maxStartPTS:u.start,d=u.duration,c=Math.max(l.end,h+Math.min(Math.max(d-this.config.maxFragLookUpTolerance,d*(this.couldBacktrack?.5:.125)),d*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(c,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case Si:case Li:case Ai:case bi:case ki:this.state=Ti}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},r.onMediaAttached=function(e,r){t.prototype.onMediaAttached.call(this,e,r);var i=r.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),i.addEventListener("playing",this.onvplaying),i.addEventListener("seeked",this.onvseeked),this.gapController=new ho(this.config,i,this.fragmentTracker,this.hls)},r.onMediaDetaching=function(){var e=this.media;e&&this.onvplaying&&this.onvseeked&&(e.removeEventListener("playing",this.onvplaying),e.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),t.prototype.onMediaDetaching.call(this)},r.onMediaPlaying=function(){this.tick()},r.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;y(e)&&this.log("Media seeked to "+e.toFixed(3));var r=this.getMainFwdBufferInfo();null!==r&&0!==r.len?this.tick():this.warn('Main forward buffer length on "seeked" event '+(r?r.len:"empty")+")")},r.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(S.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1},r.onManifestParsed=function(t,e){var r,i,n=!1,a=!1;e.levels.forEach((function(t){var e=t.audioCodec;e&&(n=n||-1!==e.indexOf("mp4a.40.2"),a=a||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=n&&a&&!("function"==typeof(null==(i=lo())||null==(r=i.prototype)?void 0:r.changeType)),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1},r.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===Ti){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=Ci)}},r.onLevelLoaded=function(t,e){var r,i=this.levels,n=e.level,a=e.details,s=a.totalduration;if(i){this.log("Level "+n+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+", cc ["+a.startCC+", "+a.endCC+"] duration:"+s);var o=i[n],l=this.fragCurrent;!l||this.state!==Li&&this.state!==Ai||l.level!==e.level&&l.loader&&this.abortCurrentFrag();var u=0;if(a.live||null!=(r=o.details)&&r.live){var h;if(this.checkLiveUpdate(a),a.deltaUpdateFailed)return;u=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details)}if(o.details=a,this.levelLastLoaded=o,this.hls.trigger(S.LEVEL_UPDATED,{details:a,level:n}),this.state===Ci){if(this.waitForCdnTuneIn(a))return;this.state=Ti}this.startFragRequested?a.live&&this.synchronizeToLiveEdge(a):this.setStartPosition(a,u),this.tick()}else this.warn("Levels were reset while loading level "+n)},r._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,n=t.payload,a=this.levels;if(a){var s=a[r.level],o=s.details;if(!o)return this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset"),void this.fragmentTracker.removeFragment(r);var l=s.videoCodec,u=o.PTSKnown||!o.live,h=null==(e=r.initSegment)?void 0:e.data,d=this._getAudioCodec(s),c=this.transmuxer=this.transmuxer||new qn(this.hls,Fe,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,v=new ii(r.level,r.sn,r.stats.chunkCount,n.byteLength,f,g),m=this.initPTS[r.cc];c.push(n,h,d,l,r,i,o.totalduration,u,v,m)}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r.onAudioTrackSwitching=function(t,e){var r=this.altAudio;if(!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var i=this.fragCurrent;i&&(this.log("Switching to main audio track, cancel main fragment load"),i.abortRequests(),this.fragmentTracker.removeFragment(i)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var n=this.hls;r&&(n.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),n.trigger(S.AUDIO_TRACK_SWITCHED,e)}},r.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var n=this.videoBuffer;n&&this.mediaBuffer!==n&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=n)}this.altAudio=i,this.tick()},r.onBufferCreated=function(t,e){var r,i,n=e.tracks,a=!1;for(var s in n){var o=n[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=n[s];l&&(this.videoBuffer=l.buffer)}}else a=!0}a&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},r.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===Fe){if(this.fragContextChanged(r))return this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state),void(this.state===ki&&(this.state=Ti));var n=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*n.total/(n.buffering.end-n.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},r.onError=function(t,e){var r;if(e.fatal)this.state=Ii;else switch(e.details){case A.FRAG_GAP:case A.FRAG_PARSING_ERROR:case A.FRAG_DECRYPT_ERROR:case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(Fe,e);break;case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:case A.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==Ci||(null==(r=e.context)?void 0:r.type)!==_e||(this.state=Ti);break;case A.BUFFER_APPEND_ERROR:case A.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===A.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case A.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}},r.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){if(this.loadedmetadata||!ri.getBuffered(t).length){var r=this.state!==Ti?this.fragCurrent:null;e.poll(this.lastCurrentTime,r)}this.lastCurrentTime=t.currentTime}},r.onFragLoadEmergencyAborted=function(){this.state=Ti,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},r.onBufferFlushed=function(t,e){var r=e.type;if(r!==O||this.audioOnly&&!this.altAudio){var i=(r===N?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,Fe),this.tick()}},r.onLevelsUpdated=function(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var t=this.media;if(t){var e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&(nT.cc;if(!1!==n.independent){var R=h.startPTS,b=h.endPTS,k=h.startDTS,D=h.endDTS;if(l)l.elementaryStreams[h.type]={startPTS:R,endPTS:b,startDTS:k,endDTS:D};else if(h.firstKeyFrame&&h.independent&&1===a.id&&!A&&(this.couldBacktrack=!0),h.dropped&&h.independent){var I=this.getMainFwdBufferInfo(),w=(I?I.end:this.getLoadPosition())+this.config.maxBufferHole,C=h.firstKeyFramePTS?h.firstKeyFramePTS:R;if(!L&&w2&&(o.gap=!0);o.setElementaryStreamInfo(h.type,R,b,k,D),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(h,o,l,a,L||A)}else{if(!L&&!A)return void this.backtrack(o);o.gap=!0}}if(v){var _=v.startPTS,x=v.endPTS,P=v.startDTS,F=v.endDTS;l&&(l.elementaryStreams[O]={startPTS:_,endPTS:x,startDTS:P,endDTS:F}),o.setElementaryStreamInfo(O,_,x,P,F),this.bufferFragmentData(v,o,l,a)}if(g&&null!=c&&null!=(e=c.samples)&&e.length){var M={id:r,frag:o,details:g,samples:c.samples};i.trigger(S.FRAG_PARSING_METADATA,M)}if(g&&d){var N={id:r,frag:o,details:g,samples:d.samples};i.trigger(S.FRAG_PARSING_USERDATA,N)}}}else this.resetWhenMissingContext(a)},r._bufferInitSegment=function(t,e,r,i){var n=this;if(this.state===bi){this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;var a=e.audio,s=e.video,o=e.audiovideo;if(a){var l=t.audioCodec,u=navigator.userAgent.toLowerCase();if(this.audioCodecSwitch){l&&(l=-1!==l.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5");var h=a.metadata;h&&"channelCount"in h&&1!==(h.channelCount||1)&&-1===u.indexOf("firefox")&&(l="mp4a.40.5")}l&&-1!==l.indexOf("mp4a.40.5")&&-1!==u.indexOf("android")&&"audio/mpeg"!==a.container&&(l="mp4a.40.2",this.log("Android: force audio codec to "+l)),t.audioCodec&&t.audioCodec!==l&&this.log('Swapping manifest audio codec "'+t.audioCodec+'" for "'+l+'"'),a.levelCodec=l,a.id="main",this.log("Init audio buffer, container:"+a.container+", codecs[selected/level/parsed]=["+(l||"")+"/"+(t.audioCodec||"")+"/"+a.codec+"]")}s&&(s.levelCodec=t.videoCodec,s.id="main",this.log("Init video buffer, container:"+s.container+", codecs[level/parsed]=["+(t.videoCodec||"")+"/"+s.codec+"]")),o&&this.log("Init audiovideo buffer, container:"+o.container+", codecs[level/parsed]=["+t.codecs+"/"+o.codec+"]"),this.hls.trigger(S.BUFFER_CODECS,e),Object.keys(e).forEach((function(t){var a=e[t].initSegment;null!=a&&a.byteLength&&n.hls.trigger(S.BUFFER_APPENDING,{type:t,data:a,frag:r,part:null,chunkMeta:i,parent:r.type})})),this.tickImmediate()}},r.getMainFwdBufferInfo=function(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,Fe)},r.backtrack=function(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=Ti},r.checkFragmentChanged=function(){var t=this.media,e=null;if(t&&t.readyState>1&&!1===t.seeking){var r=t.currentTime;if(ri.isBuffered(t,r)?e=this.getAppendedFrag(r):ri.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){this.backtrackFragment=null;var i=this.fragPlaying,n=e.level;i&&e.sn===i.sn&&i.level===n||(this.fragPlaying=e,this.hls.trigger(S.FRAG_CHANGED,{frag:e}),i&&i.level===n||this.hls.trigger(S.LEVEL_SWITCHED,{level:n}))}}},s(e,[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentFrag",get:function(){var t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}},{key:"currentProgramDateTime",get:function(){var t=this.media;if(t){var e=t.currentTime,r=this.currentFrag;if(r&&y(e)&&y(r.programDateTime)){var i=r.programDateTime+1e3*(e-r.start);return new Date(i)}}return null}},{key:"currentLevel",get:function(){var t=this.currentFrag;return t?t.level:-1}},{key:"nextBufferedFrag",get:function(){var t=this.currentFrag;return t?this.followingBufferedFrag(t):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}]),e}(_i),fo=function(){function t(e){void 0===e&&(e={}),this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new Vn,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,I(e.debug||!1,"Hls instance");var r=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var r=io(t),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(t){var i=("level"===t?"playlist":t)+"LoadPolicy",a=void 0===e[i],s=[];n.forEach((function(n){var o=t+"Loading"+n,l=e[o];if(void 0!==l&&a){s.push(o);var u=r[i].default;switch(e[i]={default:u},n){case"TimeOut":u.maxLoadTimeMs=l,u.maxTimeToFirstByteMs=l;break;case"MaxRetry":u.errorRetry.maxNumRetry=l,u.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":u.errorRetry.retryDelayMs=l,u.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":u.errorRetry.maxRetryDelayMs=l,u.timeoutRetry.maxRetryDelayMs=l}}})),s.length&&w.warn('hls.js config: "'+s.join('", "')+'" setting(s) are deprecated, use "'+i+'": '+JSON.stringify(e[i]))})),i(i({},r),e)}(t.DefaultConfig,e);this.userConfig=e,r.progressive&&no(r);var n=r.abrController,a=r.bufferController,s=r.capLevelController,o=r.errorController,l=r.fpsController,u=new o(this),h=this.abrController=new n(this),d=this.bufferController=new a(this),c=this.capLevelController=new s(this),f=new l(this),g=new Be(this),v=new $e(this),m=r.contentSteeringController,p=m?new m(this):null,y=this.levelController=new ao(this,p),E=new $r(this),T=new oo(this.config),L=this.streamController=new co(this,E,T);c.setStreamController(L),f.setStreamController(L);var A=[g,y,L];p&&A.splice(1,0,p),this.networkControllers=A;var R=[h,d,c,f,v,E];this.audioTrackController=this.createController(r.audioTrackController,A);var b=r.audioStreamController;b&&A.push(new b(this,E,T)),this.subtitleTrackController=this.createController(r.subtitleTrackController,A);var k=r.subtitleStreamController;k&&A.push(new k(this,E,T)),this.createController(r.timelineController,R),T.emeController=this.emeController=this.createController(r.emeController,R),this.cmcdController=this.createController(r.cmcdController,R),this.latencyController=this.createController(Ze,R),this.coreComponents=R,A.push(u);var D=u.onErrorOut;"function"==typeof D&&this.on(S.ERROR,D,u)}t.isMSESupported=function(){return uo()},t.isSupported=function(){return function(){if(!uo())return!1;var t=se();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((function(e){return t.isTypeSupported(he(e,"video"))}))||["mp4a.40.2","fLaC"].some((function(e){return t.isTypeSupported(he(e,"audio"))})))}()},t.getMediaSource=function(){return se()};var e=t.prototype;return e.createController=function(t,e){if(t){var r=new t(this);return e&&e.push(r),r}return null},e.on=function(t,e,r){void 0===r&&(r=this),this._emitter.on(t,e,r)},e.once=function(t,e,r){void 0===r&&(r=this),this._emitter.once(t,e,r)},e.removeAllListeners=function(t){this._emitter.removeAllListeners(t)},e.off=function(t,e,r,i){void 0===r&&(r=this),this._emitter.off(t,e,r,i)},e.listeners=function(t){return this._emitter.listeners(t)},e.emit=function(t,e,r){return this._emitter.emit(t,e,r)},e.trigger=function(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(w.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;var r=t===S.ERROR;this.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,fatal:r,event:t,error:e}),this.triggeringException=!1}}return!1},e.listenerCount=function(t){return this._emitter.listenerCount(t)},e.destroy=function(){w.log("destroy"),this.trigger(S.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((function(t){return t.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(t){return t.destroy()})),this.coreComponents.length=0;var t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null},e.attachMedia=function(t){w.log("attachMedia"),this._media=t,this.trigger(S.MEDIA_ATTACHING,{media:t})},e.detachMedia=function(){w.log("detachMedia"),this.trigger(S.MEDIA_DETACHING,void 0),this._media=null},e.loadSource=function(t){this.stopLoad();var e=this.media,r=this.url,i=this.url=p.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,w.log("loadSource:"+i),e&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(S.MANIFEST_LOADING,{url:t})},e.startLoad=function(t){void 0===t&&(t=-1),w.log("startLoad("+t+")"),this.started=!0,this.networkControllers.forEach((function(e){e.startLoad(t)}))},e.stopLoad=function(){w.log("stopLoad"),this.started=!1,this.networkControllers.forEach((function(t){t.stopLoad()}))},e.resumeBuffering=function(){this.started&&this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.startLoad(-1)}))},e.pauseBuffering=function(){this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.stopLoad()}))},e.swapAudioCodec=function(){w.log("swapAudioCodec"),this.streamController.swapAudioCodec()},e.recoverMediaError=function(){w.log("recoverMediaError");var t=this._media;this.detachMedia(),t&&this.attachMedia(t)},e.removeLevel=function(t){this.levelController.removeLevel(t)},e.setAudioOption=function(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)},e.setSubtitleOption=function(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null},s(t,[{key:"levels",get:function(){var t=this.levelController.levels;return t||[]}},{key:"currentLevel",get:function(){return this.streamController.currentLevel},set:function(t){w.log("set currentLevel:"+t),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}},{key:"nextLevel",get:function(){return this.streamController.nextLevel},set:function(t){w.log("set nextLevel:"+t),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}},{key:"loadLevel",get:function(){return this.levelController.level},set:function(t){w.log("set loadLevel:"+t),this.levelController.manualLevel=t}},{key:"nextLoadLevel",get:function(){return this.levelController.nextLoadLevel},set:function(t){this.levelController.nextLoadLevel=t}},{key:"firstLevel",get:function(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)},set:function(t){w.log("set firstLevel:"+t),this.levelController.firstLevel=t}},{key:"startLevel",get:function(){var t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t},set:function(t){w.log("set startLevel:"+t),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(t){var e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(t){this._autoLevelCapping!==t&&(w.log("set autoLevelCapping:"+t),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}},{key:"bandwidthEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimate():NaN},set:function(t){this.abrController.resetEstimator(t)}},{key:"ttfbEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(t){(function(t){return tr.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,this.levelController.checkMaxAutoUpdated())}},{key:"autoLevelEnabled",get:function(){return-1===this.levelController.manualLevel}},{key:"manualLevel",get:function(){return this.levelController.manualLevel}},{key:"minAutoLevel",get:function(){var t=this.levels,e=this.config.minAutoBitrate;if(!t)return 0;for(var r=t.length,i=0;i=e)return i;return 0}},{key:"maxAutoLevel",get:function(){var t,e=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(t=-1===r&&null!=e&&e.length?e.length-1:r,i)for(var n=t;n--;){var a=e[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return t}},{key:"firstAutoLevel",get:function(){return this.abrController.firstAutoLevel}},{key:"nextAutoLevel",get:function(){return this.abrController.nextAutoLevel},set:function(t){this.abrController.nextAutoLevel=t}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"allAudioTracks",get:function(){var t=this.audioTrackController;return t?t.allAudioTracks:[]}},{key:"audioTracks",get:function(){var t=this.audioTrackController;return t?t.audioTracks:[]}},{key:"audioTrack",get:function(){var t=this.audioTrackController;return t?t.audioTrack:-1},set:function(t){var e=this.audioTrackController;e&&(e.audioTrack=t)}},{key:"allSubtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}},{key:"subtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTrack:-1},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var t=this.subtitleTrackController;return!!t&&t.subtitleDisplay},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(t){this.config.lowLatencyMode=t}},{key:"liveSyncPosition",get:function(){return this.latencyController.liveSyncPosition}},{key:"latency",get:function(){return this.latencyController.latency}},{key:"maxLatency",get:function(){return this.latencyController.maxLatency}},{key:"targetLatency",get:function(){return this.latencyController.targetLatency}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}}],[{key:"version",get:function(){return"1.5.15"}},{key:"Events",get:function(){return S}},{key:"ErrorTypes",get:function(){return L}},{key:"ErrorDetails",get:function(){return A}},{key:"DefaultConfig",get:function(){return t.defaultConfig?t.defaultConfig:ro},set:function(e){t.defaultConfig=e}}]),t}();return fo.defaultConfig=void 0,fo},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(r="undefined"!=typeof globalThis?globalThis:r||self).Hls=i()}(!1); +//# sourceMappingURL=hls.min.js.map diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 31c602eaee5..5fda0189812 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -267,7 +267,16 @@ public final class AccountContextImpl: AccountContext { public private(set) var isPremium: Bool public let imageCache: AnyObject? +// MARK: Nicegram NCG-6373 Feed tab + private let _updateFeed = Promise() + public var updateFeed: Signal { + _updateFeed.get() + } + public func needUpdateFeed() { + _updateFeed.set(.single(())) + } +// public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions, temp: Bool = false) { self.sharedContextImpl = sharedContext @@ -578,8 +587,8 @@ public final class AccountContextImpl: AccountContext { } } - public func scheduleGroupCall(peerId: PeerId) { - let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true) + public func scheduleGroupCall(peerId: PeerId, parentController: ViewController) { + let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true, parentController: parentController) } public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 7b76b902c0d..fd05f1c3d22 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2,7 +2,6 @@ import AppLovinAdProvider import FeatNicegramHub import FeatOnboarding -import FeatTasks import NGAiChat import NGAnalytics import NGAppCache @@ -18,7 +17,6 @@ import NGRepoUser import NGStats import NGStrings import NicegramWallet -import SubscriptionAnalytics import UIKit import SwiftSignalKit @@ -397,26 +395,27 @@ private class UserInterfaceStyleObserverWindow: UIWindow { self.buildConfig = buildConfig let signatureDict = BuildConfigExtra.signatureDict() - let mobyApiKey = NGENV.moby_key - MobySubscriptionAnalytics.setup(apiKey: mobyApiKey) { account in - account.appsflyerID = nil - } completion: { _, _ in } - - if AppCache.appLaunchCount == 1 { - let installTime = (time_t)(Date().timeIntervalSince1970) - let installInfo = InstallInfo(installDateTimestamp: installTime) - MobySubscriptionAnalytics.trackInstall(installInfo: installInfo) + // MARK: Nicegram, moved 'isDebugConfiguration' definition here + var isDebugConfiguration = false + #if DEBUG + isDebugConfiguration = true + #endif + + if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { + isDebugConfiguration = true } + // + let ngEnableLogging = isDebugConfiguration NGEntryPoint.onAppLaunch( env: Env( apiBaseUrl: URL(string: NGENV.ng_api_url)!, apiKey: NGENV.ng_api_key, + enableLogging: ngEnableLogging, isAppStoreBuild: buildConfig.isAppStoreBuild, premiumProductId: NGENV.premium_bundle, privacyUrl: URL(string: NGENV.privacy_url)!, referralBot: NGENV.referral_bot, - tapjoyApiKey: NGENV.tapjoy_api_key, telegramAuthBot: NGENV.telegram_auth_bot, termsUrl: URL(string: NGENV.terms_url)!, webSocketUrl: NGENV.websocket_url @@ -445,14 +444,16 @@ private class UserInterfaceStyleObserverWindow: UIWindow { env: { .init( appUrlScheme: buildConfig.appSpecificUrlScheme, - enableLogging: false, + enableLogging: ngEnableLogging, keychainGroupIdentifier: NGENV.wallet.keychainGroupIdentifier, nicegramApiBaseUrl: URL(string: NGENV.ng_api_url)! .appendingPathComponent("v7/"), walletConnectProjectId: NGENV.wallet.walletConnectProjectId, web3AuthBackupQuestion: NGENV.wallet.web3AuthBackupQuestion, web3AuthClientId: NGENV.wallet.web3AuthClientId, - web3AuthVerifier: NGENV.wallet.web3AuthVerifier + web3AuthVerifier: NGENV.wallet.web3AuthVerifier, + stonfiApiUrl: NGENV.wallet.stonfiApiUrl, + stonfiNicegramApiUrl: NGENV.wallet.stonfiNicegramApiUrl ) }, contactImageProvider: { @@ -689,15 +690,8 @@ private class UserInterfaceStyleObserverWindow: UIWindow { self.mainWindow?.presentNative(UIAlertController(title: nil, message: "Error 2", preferredStyle: .alert)) return true } - - var isDebugConfiguration = false - #if DEBUG - isDebugConfiguration = true - #endif - - if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { - isDebugConfiguration = true - } + + // MARK: Nicegram, moved 'isDebugConfiguration' definition up if isDebugConfiguration || buildConfig.isInternalBuild { LoggingSettings.defaultSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true) @@ -1323,10 +1317,6 @@ private class UserInterfaceStyleObserverWindow: UIWindow { RepoTgHelper.setTelegramId( accountContext.account.peerId.id._internalGetInt64Value() ) - - TasksContainer.shared.channelSubscriptionChecker.register { - ChannelSubscriptionCheckerImpl(context: accountContext) - } } if #available(iOS 13.0, *) { @@ -1849,6 +1839,12 @@ private class UserInterfaceStyleObserverWindow: UIWindow { let timestamp = Int(CFAbsoluteTimeGetCurrent()) let minReindexTimestamp = timestamp - 2 * 24 * 60 * 60 if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp") as? NSNumber, indexTimestamp.intValue >= minReindexTimestamp { + #if DEBUG && false + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground") + let _ = self.runCacheReindexTasks(lowImpact: true, completion: { + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done1") + }) + #endif } else { UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp") @@ -2110,7 +2106,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { if resetOnce { resetOnce = false if count == 0 { - UIApplication.shared.applicationIconBadgeNumber = 1 + //UIApplication.shared.applicationIconBadgeNumber = 1 } } UIApplication.shared.applicationIconBadgeNumber = Int(count) @@ -2167,14 +2163,19 @@ private class UserInterfaceStyleObserverWindow: UIWindow { self.isActiveValue = false self.isActivePromise.set(false) - var taskId: UIBackgroundTaskIdentifier? - taskId = application.beginBackgroundTask(withName: "lock", expirationHandler: { - if let taskId = taskId { + final class TaskIdHolder { + var taskId: UIBackgroundTaskIdentifier? + } + + let taskIdHolder = TaskIdHolder() + + taskIdHolder.taskId = application.beginBackgroundTask(withName: "lock", expirationHandler: { + if let taskId = taskIdHolder.taskId { UIApplication.shared.endBackgroundTask(taskId) } }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: { - if let taskId = taskId { + if let taskId = taskIdHolder.taskId { UIApplication.shared.endBackgroundTask(taskId) } }) @@ -2753,7 +2754,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { if let primary = primary { for context in contexts { if let context = context, context.account.id == primary { - self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil) + self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true) return } } @@ -2761,7 +2762,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { for context in contexts { if let context = context { - self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil) + self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true) return } } @@ -2859,7 +2860,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { })) } - private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) { + private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) { let signal = self.sharedContextPromise.get() |> take(1) |> deliverOnMainQueue @@ -2878,7 +2879,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { } self.openChatWhenReadyDisposable.set((signal |> deliverOnMainQueue).start(next: { context in - context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId) + context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny) })) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 245323180c6..aef09926204 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -154,7 +154,7 @@ final class AuthorizedApplicationContext { private var showCallsTab: Bool private var showCallsTabDisposable: Disposable? private var enablePostboxTransactionsDiposable: Disposable? - + init(sharedApplicationContext: SharedApplicationContext, mainWindow: Window1, watchManagerArguments: Signal, context: AccountContextImpl, accountManager: AccountManager, showCallsTab: Bool, reinitializedNotificationSettings: @escaping () -> Void) { self.sharedApplicationContext = sharedApplicationContext @@ -894,7 +894,7 @@ final class AuthorizedApplicationContext { })) } - func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) { + func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) { if let storyId { var controllers = self.rootController.viewControllers controllers = controllers.filter { c in @@ -945,7 +945,11 @@ final class AuthorizedApplicationContext { chatLocation = .peer(peer) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) + if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { + self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) + } else { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) + } }) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 0ebecb51155..09b4bf76a67 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -126,7 +126,7 @@ import AdsInfoScreen extension ChatControllerImpl { func loadDisplayNodeImpl() { - self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self) + self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self, isFeed: self.isFeed) if let currentItem = self.tempVoicePlaylistCurrentItem { self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem) @@ -180,11 +180,17 @@ extension ChatControllerImpl { } + self.chatDisplayNode.historyNode.hasAtLeast3MessagesUpdated = { [weak self] hasAtLeast3Messages in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasAtLeast3Messages(hasAtLeast3Messages) }) + } + } self.chatDisplayNode.historyNode.hasPlentyOfMessagesUpdated = { [weak self] hasPlentyOfMessages in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) } } + if case .peer(self.context.account.peerId) = self.chatLocation { var didDisplayTooltip = false if "".isEmpty { @@ -1120,9 +1126,16 @@ extension ChatControllerImpl { self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f, messageCorrelationId in //print("setup layoutActionOnViewTransition") - self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in + guard let self else { + return + } + self.layoutActionOnViewTransitionAction = f + + self.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in f() if let strongSelf = self, let validLayout = strongSelf.validLayout { + strongSelf.layoutActionOnViewTransitionAction = nil + var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? let isScheduledMessages: Bool @@ -1262,35 +1275,75 @@ extension ChatControllerImpl { } } - let signal: Signal<[MessageId?], NoError> - if forwardSourcePeerIds.count > 1 { - var signals: [Signal<[MessageId?], NoError>] = [] - for messagesGroup in forwardedMessages { - signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) - } - signal = combineLatest(signals) - |> map { results in - var ids: [MessageId?] = [] - for result in results { - ids.append(contentsOf: result) + let _ = (strongSelf.shouldDivertMessagesToScheduled(messages: transformedMessages) + |> deliverOnMainQueue).start(next: { shouldDivert in + let signal: Signal<[MessageId?], NoError> + var shouldOpenScheduledMessages = false + if forwardSourcePeerIds.count > 1 { + var forwardedMessages = forwardedMessages + if shouldDivert { + forwardedMessages = forwardedMessages.map { messageGroup -> [EnqueueMessage] in + return messageGroup.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } + } + } + shouldOpenScheduledMessages = true } - return ids + + var signals: [Signal<[MessageId?], NoError>] = [] + for messagesGroup in forwardedMessages { + signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) + } + signal = combineLatest(signals) + |> map { results in + var ids: [MessageId?] = [] + for result in results { + ids.append(contentsOf: result) + } + return ids + } + } else { + var transformedMessages = transformedMessages + if shouldDivert { + transformedMessages = transformedMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } + } + shouldOpenScheduledMessages = true + } + + signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) } - } else { - signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) - } - - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { + + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { messageIds in + guard let strongSelf = self else { + return + } if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { } else { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + + if shouldOpenScheduledMessages { + if let layoutActionOnViewTransitionAction = strongSelf.layoutActionOnViewTransitionAction { + strongSelf.layoutActionOnViewTransitionAction = nil + layoutActionOnViewTransitionAction() + } + } } - } + }) + + donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) }) - - donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } else if case let .customChatContents(customChatContents) = strongSelf.subject { switch customChatContents.kind { case .hashTagSearch: @@ -1390,7 +1443,7 @@ extension ChatControllerImpl { if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).startStandalone(next: { message in - guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { + guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState else { return } var originalMediaReference: AnyMediaReference? @@ -1405,7 +1458,11 @@ extension ChatControllerImpl { } } } - strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference) + var editMediaOptions: MessageMediaEditingOptions? + if case let .media(options) = editMessageState.content { + editMediaOptions = options + } + strongSelf.presentEditingAttachmentMenu(editMediaOptions: editMediaOptions, editMediaReference: originalMediaReference) }) } else { strongSelf.presentAttachmentMenu(subject: .default) @@ -2012,51 +2069,47 @@ extension ChatControllerImpl { } }, reportSelectedMessages: { [weak self] in if let strongSelf = self, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - if let reportReason = strongSelf.presentationInterfaceState.reportReason { + if let (_, option, message) = strongSelf.presentationInterfaceState.reportReason { let presentationData = strongSelf.presentationData - let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true) - let dismissAction: () -> Void = { [weak self, weak controller] in - self?.view.window?.endEditing(true) - controller?.dismissAnimated() - } - var message = "" - var items: [ActionSheetItem] = [] - items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in - message = text - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { - dismissAction() - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in - let _ = (strongSelf.context.engine.peers.reportPeerMessages(messageIds: Array(messageIds), reason: reportReason, message: message) - |> deliverOnMainQueue).startStandalone(completed: { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) - }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in + let _ = (strongSelf.context.engine.messages.reportContent(subject: .messages(Array(messageIds)), option: option, message: message) + |> deliverOnMainQueue).startStandalone(completed: { + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) }) - })) - - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - strongSelf.present(controller, in: .window(.root)) + }) } else { - strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in - self?.present(c, in: .window(.root), with: a) - }, push: { c in - self?.push(c) - }, completion: { _, done in - if done { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - } - }), in: .window(.root)) + strongSelf.context.sharedContext.makeContentReportScreen( + context: strongSelf.context, + subject: .messages(Array(messageIds).sorted()), + forceDark: false, + present: { [weak self] controller in + self?.push(controller) + }, + completion: { [weak self] in + self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + }, + requestSelectMessages: nil + ) } } }, reportMessages: { [weak self] messages, contextController in - if let strongSelf = self, !messages.isEmpty { - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in }) + guard let self, !messages.isEmpty else { + return } + contextController?.dismiss() + self.context.sharedContext.makeContentReportScreen( + context: self.context, + subject: .messages(messages.map({ $0.id }).sorted()), + forceDark: false, + present: { [weak self] controller in + guard let self else { + return + } + self.push(controller) + }, + completion: {}, + requestSelectMessages: nil + ) }, blockMessageAuthor: { [weak self] message, contextController in contextController?.dismiss(completion: { guard let strongSelf = self else { @@ -4146,7 +4199,7 @@ extension ChatControllerImpl { } var isBot = false for message in messages { - if let author = message.author, case let .user(user) = author, user.botInfo != nil { + if let author = message.author, case let .user(user) = author, user.botInfo != nil && !user.id.isVerificationCodes { isBot = true break } @@ -4155,7 +4208,7 @@ extension ChatControllerImpl { if isBot { type = .bot } else if let user = peer as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user @@ -4266,12 +4319,7 @@ extension ChatControllerImpl { guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { return } - var langCode = langCode - if langCode == "nb" { - langCode = "no" - } else if langCode == "pt-br" { - langCode = "pt" - } + let langCode = normalizeTranslationLanguage(langCode) let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in return current?.withToLang(langCode).withIsEnabled(true) }).startStandalone() @@ -4624,22 +4672,62 @@ extension ChatControllerImpl { if let peerId = peerId { self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId) - |> deliverOnMainQueue).startStrict(next: { [weak self] namespace, silent in - if let strongSelf = self { - let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } - if inAppNotificationSettings.playSounds && !silent { - serviceSoundManager.playMessageDeliveredSound() + |> deliverOnMainQueue).startStrict(next: { [weak self] eventGroup in + guard let self else { + return + } + let inAppNotificationSettings = self.context.sharedContext.currentInAppNotificationSettings.with { $0 } + if inAppNotificationSettings.playSounds, let firstEvent = eventGroup.first, !firstEvent.isSilent { + serviceSoundManager.playMessageDeliveredSound() + } + if self.presentationInterfaceState.subject != .scheduledMessages, let firstEvent = eventGroup.first, firstEvent.id.namespace == Namespaces.Message.ScheduledCloud { + if eventGroup.contains(where: { $0.isPendingProcessing }) { + self.openScheduledMessages(completion: { [weak self] c in + guard let self else { + return + } + + c.dismissAllUndoControllers() + + Queue.mainQueue().after(0.5) { [weak c] in + c?.displayProcessingVideoTooltip(messageId: firstEvent.id) + } + + c.present( + UndoOverlayController( + presentationData: self.presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Chat/ToastImprovingVideo"), color: .white)!, + size: nil, + title: self.presentationData.strings.Chat_ToastImprovingVideo_Title, + text: self.presentationData.strings.Chat_ToastImprovingVideo_Text, + customUndoText: nil, + timeout: 5.0 + ), + elevatedLayout: false, + position: .top, + action: { _ in + return true + } + ), + in: .current + ) + }) } - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud { - strongSelf.openScheduledMessages() + } + + if self.shouldDisplayChecksTooltip { + Queue.mainQueue().after(1.0) { [weak self] in + self?.displayChecksTooltip() } - - if strongSelf.shouldDisplayChecksTooltip { - Queue.mainQueue().after(1.0) { - strongSelf.displayChecksTooltip() - } - strongSelf.shouldDisplayChecksTooltip = false - strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) + self.shouldDisplayChecksTooltip = false + self.checksTooltipDisposable.set(self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) + } + + if let shouldDisplayProcessingVideoTooltip = self.shouldDisplayProcessingVideoTooltip { + self.shouldDisplayProcessingVideoTooltip = nil + Queue.mainQueue().after(1.0) { [weak self] in + self?.displayProcessingVideoTooltip(messageId: shouldDisplayProcessingVideoTooltip) } } })) @@ -4948,6 +5036,20 @@ extension ChatControllerImpl { }), in: .current) }) + if case .scheduledMessages = self.subject { + self.postedScheduledMessagesEventsDisposable = (self.context.account.stateManager.sentScheduledMessageIds + |> deliverOnMainQueue).start(next: { [weak self] ids in + guard let self, let peerId = self.chatLocation.peerId else { + return + } + let filteredIds = Array(ids).filter({ $0.peerId == peerId }) + if filteredIds.isEmpty { + return + } + self.displayPostedScheduledMessagesToast(ids: filteredIds) + }) + } + self.displayNodeDidLoad() } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 143746e1933..ac4358c04a8 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -346,7 +346,7 @@ extension ChatControllerImpl { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) } - strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -521,7 +521,7 @@ extension ChatControllerImpl { attributes.append(EffectMessageAttribute(id: messageEffect.id)) } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 3d452c8b5ac..a73ee8d4a4b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -105,6 +105,10 @@ extension ChatControllerImpl { } } } + + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + tip = .videoProcessing + } if actions.tip == nil { actions.tip = tip diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index b0374e4646a..1fec78f1939 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -14,7 +14,7 @@ import UndoUI import UrlHandling import TelegramPresentationData -func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { +func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { let presentationData: PresentationData if let parentController = parentController as? ChatControllerImpl { presentationData = parentController.presentationData @@ -109,8 +109,8 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u var presentImpl: ((ViewController, Any?) -> Void)? let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) - let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in @@ -135,22 +135,16 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } }, didDismiss: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl { -// let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false -// parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer) -// if isFocused { -// parentController.chatDisplayNode.textInputPanelNode?.ensureFocused() -// } - parentController.updateChatPresentationInterfaceState(interactive: false) { state in return state.updatedForceInputCommandsHidden(false) } } }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal parentController.push(controller) @@ -180,10 +174,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } let webViewSignal: Signal + let webViewSource: RequestSimpleWebViewSource = isInline ? .inline(startParam: payload) : .generic if url.isEmpty { - webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } else { - webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } messageActionCallbackDisposable.set(((webViewSignal @@ -201,19 +196,19 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal if let parentController = parentController as? ChatControllerImpl { @@ -242,14 +237,14 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u |> afterDisposed { updateProgress() }) - |> deliverOnMainQueue).startStrict(next: { [weak parentController] result in + |> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in guard let parentController else { return } var presentImpl: ((ViewController, Any?) -> Void)? let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, completion: { [weak parentController] in @@ -257,11 +252,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() } }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal if let parentController = parentController as? ChatControllerImpl { @@ -301,7 +296,9 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() openWebView() }, showMore: nil, openTerms: { - + if let navigationController = parentController.navigationController as? NavigationController { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } }) parentController.present(controller, in: .window(.root)) } @@ -316,7 +313,7 @@ public extension ChatControllerImpl { } self.chatDisplayNode.dismissInput() - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil) } static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { @@ -381,7 +378,7 @@ public extension ChatControllerImpl { }) } - static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { + static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, forceUpdate: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { if let controller { controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } else { @@ -394,7 +391,7 @@ public extension ChatControllerImpl { } else if let main = context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController = main } - context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: forceUpdate, openPeer: { peer, navigation in if let navigationController { ChatControllerImpl.botOpenPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) } @@ -477,8 +474,8 @@ public extension ChatControllerImpl { let context = strongSelf.context let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) var presentImpl: ((ViewController, Any?) -> Void)? - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak self] query, chatTypes, completion in @@ -567,7 +564,7 @@ public extension ChatControllerImpl { } }) } else { - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 8eacbdcc2a7..9e251e8af9d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -10,6 +10,7 @@ import MediaPickerUI import MediaPasteboardUI import LegacyMediaPickerUI import MediaEditor +import ChatEntityKeyboardInputNode extension ChatControllerImpl { func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) { @@ -32,7 +33,13 @@ extension ChatControllerImpl { }) } }, - getSourceRect: nil + getSourceRect: nil, + makeEntityInputView: { [weak self] in + guard let self else { + return nil + } + return EntityInputView(context: self.context, isDark: false, areCustomEmojiEnabled: self.presentationInterfaceState.customEmojiAvailable) + } ) controller.navigationPresentation = .flatModal strongSelf.push(controller) @@ -88,7 +95,7 @@ extension ChatControllerImpl { fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject @@ -211,7 +218,7 @@ extension ChatControllerImpl { var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) let previewRepresentations: [TelegramMediaImageRepresentation] = [] // if let thumbnailResource { @@ -220,7 +227,7 @@ extension ChatControllerImpl { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: 0, attributes: fileAttributes) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: 0, attributes: fileAttributes, alternativeRepresentations: []) self.enqueueStickerFile(file) default: break diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift new file mode 100644 index 00000000000..f2ddc3926bb --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift @@ -0,0 +1,100 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import UndoUI +import AccountContext +import ChatControllerInteraction + +extension ChatControllerImpl { + func displayPostedScheduledMessagesToast(ids: [EngineMessage.Id]) { + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastPostedScheduledMessagesToastTimestamp + 0.4 >= timestamp { + return + } + self.lastPostedScheduledMessagesToastTimestamp = timestamp + + guard case .scheduledMessages = self.presentationInterfaceState.subject else { + return + } + + let _ = (self.context.engine.data.get( + EngineDataList(ids.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + guard let self else { + return + } + let messages = messages.compactMap { $0 } + + var found: (message: EngineMessage, file: TelegramMediaFile)? + outer: for message in messages { + for media in message.media { + if let file = media as? TelegramMediaFile, file.isVideo { + found = (message, file) + break outer + } + } + } + + guard let (message, file) = found else { + return + } + + guard case let .loaded(isEmpty, _) = self.chatDisplayNode.historyNode.currentHistoryState else { + return + } + + if isEmpty { + if let navigationController = self.navigationController as? NavigationController, let topController = navigationController.viewControllers.first(where: { c in + if let c = c as? ChatController, c.chatLocation == self.chatLocation { + return true + } + return false + }) as? ChatControllerImpl { + topController.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: self.presentationData.strings.Chat_ToastVideoPublished_Title, + undoText: nil, + customAction: nil + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + + self.dismiss() + } + } else { + self.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: self.presentationData.strings.Chat_ToastVideoPublished_Title, + undoText: self.presentationData.strings.Chat_ToastVideoPublished_Action, + customAction: { [weak self] in + guard let self else { + return + } + self.dismiss() + } + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatAdPanelNode.swift b/submodules/TelegramUI/Sources/ChatAdPanelNode.swift new file mode 100644 index 00000000000..c3e42603d30 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatAdPanelNode.swift @@ -0,0 +1,520 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import StickerResources +import PhotoResources +import TelegramStringFormatting +import AnimatedCountLabelNode +import AnimatedNavigationStripeNode +import ContextUI +import RadialStatusNode +import TextFormat +import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import TranslateUI +import ChatControllerInteraction + +private enum PinnedMessageAnimation { + case slideToTop + case slideToBottom +} + +final class ChatAdPanelNode: ASDisplayNode { + private let context: AccountContext + private(set) var message: Message? + + var controllerInteraction: ChatControllerInteraction? + + private let tapButton: HighlightTrackingButtonNode + + private let contextContainer: ContextControllerSourceNode + private let clippingContainer: ASDisplayNode + private let contentContainer: ASDisplayNode + private let contentTextContainer: ASDisplayNode + private let adNode: TextNode + private let titleNode: TextNode + private let textNode: TextNodeWithEntities + + private let removeButtonNode: HighlightTrackingButtonNode + private let removeBackgroundNode: ASImageNode + private let removeTextNode: ImmediateTextNode + + private let closeButton: HighlightableButtonNode + + private let imageNode: TransformImageNode + private let imageNodeContainer: ASDisplayNode + + private let separatorNode: ASDisplayNode + + private var currentLayout: (CGFloat, CGFloat, CGFloat)? + private var currentMessage: Message? + private var previousMediaReference: AnyMediaReference? + + private let fetchDisposable = MetaDisposable() + + private let animationCache: AnimationCache? + private let animationRenderer: MultiAnimationRenderer? + + init(context: AccountContext, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + + self.tapButton = HighlightTrackingButtonNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.contextContainer = ContextControllerSourceNode() + + self.clippingContainer = ASDisplayNode() + self.clippingContainer.clipsToBounds = true + + self.contentContainer = ASDisplayNode() + self.contentTextContainer = ASDisplayNode() + + self.adNode = TextNode() + self.adNode.displaysAsynchronously = false + self.adNode.isUserInteractionEnabled = false + + self.removeButtonNode = HighlightTrackingButtonNode() + self.removeBackgroundNode = ASImageNode() + + self.removeTextNode = ImmediateTextNode() + self.removeTextNode.displaysAsynchronously = false + self.removeTextNode.isUserInteractionEnabled = false + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.isUserInteractionEnabled = false + + self.textNode = TextNodeWithEntities() + self.textNode.textNode.displaysAsynchronously = false + self.textNode.textNode.isUserInteractionEnabled = false + + self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + + self.imageNodeContainer = ASDisplayNode() + + self.closeButton = HighlightableButtonNode() + self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.closeButton.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.contextContainer) + + self.contextContainer.addSubnode(self.clippingContainer) + self.clippingContainer.addSubnode(self.contentContainer) + self.contentTextContainer.addSubnode(self.titleNode) + + self.contentTextContainer.addSubnode(self.adNode) + + self.contentTextContainer.addSubnode(self.textNode.textNode) + self.contentContainer.addSubnode(self.contentTextContainer) + + self.imageNodeContainer.addSubnode(self.imageNode) + self.contentContainer.addSubnode(self.imageNodeContainer) + + self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) + self.tapButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.adNode.layer.removeAnimation(forKey: "opacity") + strongSelf.adNode.alpha = 0.4 + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity") + strongSelf.textNode.textNode.alpha = 0.4 + strongSelf.imageNode.layer.removeAnimation(forKey: "opacity") + strongSelf.imageNode.alpha = 0.4 + strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeTextNode.alpha = 0.4 + strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeBackgroundNode.alpha = 0.4 + } else { + strongSelf.adNode.alpha = 1.0 + strongSelf.adNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.textNode.textNode.alpha = 1.0 + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.imageNode.alpha = 1.0 + strongSelf.imageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeTextNode.alpha = 1.0 + strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeBackgroundNode.alpha = 1.0 + strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.contextContainer.addSubnode(self.tapButton) + + self.contextContainer.addSubnode(self.removeBackgroundNode) + self.contextContainer.addSubnode(self.removeTextNode) + self.contextContainer.addSubnode(self.removeButtonNode) + + self.addSubnode(self.separatorNode) + + self.removeButtonNode.addTarget(self, action: #selector(self.removePressed), forControlEvents: [.touchUpInside]) + self.removeButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeTextNode.alpha = 0.4 + strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeBackgroundNode.alpha = 0.4 + } else { + strongSelf.removeTextNode.alpha = 1.0 + strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeBackgroundNode.alpha = 1.0 + strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.contextContainer.activated = { [weak self] gesture, _ in + guard let self, let message = self.message else { + return + } + self.controllerInteraction?.adContextAction(message, self.contextContainer, gesture) + } + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + } + + deinit { + self.fetchDisposable.dispose() + } + + private var theme: PresentationTheme? + + @objc private func closePressed() { + if self.context.isPremium, let adAttribute = self.message?.adAttribute { + self.controllerInteraction?.removeAd(adAttribute.opaqueId) + } else { + self.controllerInteraction?.openNoAdsDemo() + } + } + + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.message = interfaceState.adMessage + + if self.theme !== interfaceState.theme { + self.theme = interfaceState.theme + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + self.removeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 15.0, color: interfaceState.theme.chat.inputPanel.panelControlAccentColor.withMultipliedAlpha(0.1)) + self.removeTextNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_BotAd_WhatIsThis, font: Font.regular(11.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) + } + + self.contextContainer.isGestureEnabled = false + + let panelHeight: CGFloat + var hasCloseButton = true + if let message = interfaceState.adMessage { + panelHeight = self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: nil, message: message, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: false, isReplyThread: false, translateToLanguage: nil) + hasCloseButton = message.media.isEmpty + } else { + panelHeight = 50.0 + } + + self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + let contentRightInset: CGFloat = 14.0 + rightInset + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize) + + self.closeButton.isHidden = !hasCloseButton + + self.currentLayout = (width, leftInset, rightInset) + + return panelHeight + } + + private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, message: Message, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) -> CGFloat { + var animationTransition: ContainedViewLayoutTransition = .immediate + + if let animation = animation { + animationTransition = .animated(duration: 0.2, curve: .easeInOut) + + if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) { + let offset: CGFloat + switch animation { + case .slideToTop: + offset = -10.0 + case .slideToBottom: + offset = 10.0 + } + + copyView.frame = self.textNode.textNode.frame + self.textNode.textNode.view.superview?.addSubview(copyView) + copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in + copyView?.removeFromSuperview() + }) + self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true) + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let makeAdLayout = TextNode.asyncLayout(self.adNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let imageNodeLayout = self.imageNode.asyncLayout() + + let previousMediaReference = self.previousMediaReference + let context = self.context + + let contentLeftInset: CGFloat = leftInset + 18.0 + let contentRightInset: CGFloat = rightInset + 9.0 + + var textRightInset: CGFloat = 0.0 + + var updatedMediaReference: AnyMediaReference? + var imageDimensions: CGSize? + + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMediaReference = .message(message: MessageReference(message), media: image) + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = media as? TelegramMediaFile { + updatedMediaReference = .message(message: MessageReference(message), media: file) + if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = representation.dimensions.cgSize + } else if file.isAnimated, let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { + switch firstMedia { + case let .preview(dimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + if let dimensions { + imageDimensions = dimensions.cgSize + } + updatedMediaReference = .standalone(media: thumbnailMedia) + case let .full(fullMedia): + updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) + if let image = fullMedia as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = fullMedia as? TelegramMediaFile { + if let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } + } + } + } + } + + let imageBoundingSize = CGSize(width: 48.0, height: 48.0) + var applyImage: (() -> Void)? + if let imageDimensions { + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 3.0), imageSize: imageDimensions.aspectFilled(imageBoundingSize), boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) + textRightInset += imageBoundingSize.width + 18.0 + } + + var mediaUpdated = false + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) + } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { + mediaUpdated = true + } + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchMediaSignal: Signal? + if mediaUpdated { + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + if imageReference.media.representations.isEmpty { + updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) + } else { + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: false) + } + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isAnimatedSticker { + let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) + updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) + } else if fileReference.media.isVideo || fileReference.media.isAnimated { + updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + + let textConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + + let (adLayout, adApply) = makeAdLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Chat_BotAd_Title, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + var titleText: String = "" + if let author = message.author { + titleText = EnginePeer(author).compactDisplayTitle + } + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) + + let messageText: NSAttributedString + let textFont = Font.regular(14.0) + if isText { + var text = message.text + var messageEntities = message.textEntitiesAttribute?.entities ?? [] + + if let translateToLanguage = translateToLanguage, !text.isEmpty { + for attribute in message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + text = attribute.text + messageEntities = attribute.entities + break + } + } + } + + let entities = messageEntities.filter { entity in + switch entity.type { + case .CustomEmoji: + return true + default: + return false + } + } + let textColor = theme.chat.inputPanel.primaryTextColor + if entities.count > 0 { + messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + } else { + messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor) + } + } else { + messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + var panelHeight: CGFloat = 0.0 + if let _ = imageDimensions { + panelHeight = 9.0 + imageBoundingSize.height + 9.0 + } + + var textHeight: CGFloat + var titleOnSeparateLine = false + if textLayout.numberOfLines == 1 || contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width > width - contentRightInset - textRightInset { + textHeight = adLayout.size.height + titleLayout.size.height + textLayout.size.height + 15.0 + titleOnSeparateLine = true + } else { + textHeight = titleLayout.size.height + textLayout.size.height + 15.0 + } + + panelHeight = max(panelHeight, textHeight) + + Queue.mainQueue().async { + let _ = adApply() + let _ = titleApply() + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = self.animationCache, let renderer = self.animationRenderer { + textArguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: cache, + renderer: renderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + } + let _ = textApply(textArguments) + + self.previousMediaReference = updatedMediaReference + + let textContainerFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: width, height: panelHeight)) + animationTransition.updateFrameAdditive(node: self.contentTextContainer, frame: textContainerFrame) + + let removeTextSize = self.removeTextNode.updateLayout(CGSize(width: width, height: .greatestFiniteMagnitude)) + + if titleOnSeparateLine { + self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size) + self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: titleLayout.size) + self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 43.0), size: textLayout.size) + + self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize) + self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0) + self.removeButtonNode.frame = self.removeBackgroundNode.frame + } else { + self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size) + self.titleNode.frame = CGRect(origin: CGPoint(x: adLayout.size.width + 2.0, y: 9.0), size: titleLayout.size) + self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: textLayout.size) + + self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize) + self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0) + self.removeButtonNode.frame = self.removeBackgroundNode.frame + } + + self.textNode.visibilityRect = CGRect.infinite + + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: width - contentRightInset - imageBoundingSize.width, y: 9.0), size: imageBoundingSize) + self.imageNode.frame = CGRect(origin: CGPoint(), size: imageBoundingSize) + + if let applyImage = applyImage { + applyImage() + + animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 1.0) + animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true) + } else { + animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 0.1) + animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true) + } + + if let updateImageSignal = updateImageSignal { + self.imageNode.setSignal(updateImageSignal) + } + if let updatedFetchMediaSignal = updatedFetchMediaSignal { + self.fetchDisposable.set(updatedFetchMediaSignal.startStrict()) + } + } + + return panelHeight + } + + @objc func tapped() { + guard let message = self.message else { + return + } + self.controllerInteraction?.activateAdAction(message.id, nil, false, false) + } + + @objc func removePressed() { + guard let message = self.message else { + return + } + self.controllerInteraction?.adContextAction(message, self.contextContainer, nil) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a604a470a38..a47b22b6a07 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -54,6 +54,7 @@ import TooltipUI import StatisticsUI import NGWebUtils // MARK: Nicegram Imports +import FeatTgUtils import NGAppCache import NGStrings import NGUI @@ -141,6 +142,8 @@ import OwnershipTransferController import OldChannelsController import BrowserUI import NotificationPeerExceptionController +import AdsReportScreen +import AdUI public enum ChatControllerPeekActions { case standard @@ -341,6 +344,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let startingBot = ValuePromise(false, ignoreRepeated: true) let unblockingPeer = ValuePromise(false, ignoreRepeated: true) public let searching = ValuePromise(false, ignoreRepeated: true) + public let searchResultsCount = ValuePromise(0, ignoreRepeated: true) let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() let loadingMessage = Promise(nil) let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) @@ -458,6 +462,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let checksTooltipDisposable = MetaDisposable() var shouldDisplayChecksTooltip = false + var shouldDisplayProcessingVideoTooltip: EngineMessage.Id? let peerSuggestionsDisposable = MetaDisposable() let peerSuggestionsDismissDisposable = MetaDisposable() @@ -490,6 +495,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G weak var copyProtectionTooltipController: TooltipController? weak var emojiPackTooltipController: TooltipScreen? weak var birthdayTooltipController: TooltipScreen? + weak var scheduledVideoProcessingTooltipController: TooltipScreen? weak var slowmodeTooltipController: ChatSlowmodeHintController? @@ -617,6 +623,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var powerSavingMonitoringDisposable: Disposable? + // MARK: Nicegram + private var nicegramCloseCallback: (() -> Void)? + // + var avatarNode: ChatAvatarNavigationNode? var storyStats: PeerStoryStats? @@ -640,6 +650,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + public var externalSearchResultsCount: Int32? { + didSet { + if let panelNode = self.chatDisplayNode.inputPanelNode as? ChatTagSearchInputPanelNode { + panelNode.externalSearchResultsCount = self.externalSearchResultsCount + } + } + } + public var includeSavedPeersInSearchResults: Bool = false { didSet { self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults @@ -651,6 +669,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.showListEmptyResults = self.showListEmptyResults } } +// MARK: Nicegram NCG-6373 Feed tab + let isFeed: Bool +// +// MARK: Nicegram NCG-6373 Feed tab, isFeed + + var layoutActionOnViewTransitionAction: (() -> Void)? + + var lastPostedScheduledMessagesToastTimestamp: Double = 0.0 + var postedScheduledMessagesEventsDisposable: Disposable? public init( context: AccountContext, @@ -666,8 +693,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], customChatNavigationStack: [EnginePeer.Id]? = nil, - params: ChatControllerParams? = nil + params: ChatControllerParams? = nil, + isFeed: Bool = false ) { +// MARK: Nicegram NCG-6373 Feed tab + self.isFeed = isFeed +// let _ = ChatControllerCount.modify { value in return value + 1 } @@ -1093,7 +1124,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatSchedule, action: { if let strongSelf = self { - strongSelf.context.scheduleGroupCall(peerId: message.id.peerId) + strongSelf.context.scheduleGroupCall(peerId: message.id.peerId, parentController: strongSelf) } }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) } @@ -1133,12 +1164,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } - /*for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - //strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) - break - } - }*/ return true case .setChatTheme: strongSelf.presentThemeSelection() @@ -1198,18 +1223,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.push(wallpaperPreviewController) return true - case let .giftPremium(_, _, duration, _, _): + case let .giftPremium(_, _, duration, _, _, _, _): strongSelf.chatDisplayNode.dismissInput() let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil)) strongSelf.push(controller) return true + case .starGift: + let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message)) + strongSelf.push(controller) + return true case .giftStars: let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message)) strongSelf.push(controller) return true - case let .giftCode(slug, _, _, _, _, _, _, _, _): + case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true case .prizeStars: @@ -1300,6 +1329,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G standalone = true } + if let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute { + if let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile, file.isVideo && !file.isAnimated { + strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: true, fullscreen: false) + } else { + strongSelf.controllerInteraction?.activateAdAction(message.id, nil, true, false) + return true + } + } + return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a, i in @@ -1411,7 +1449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, openAd: { [weak self] messageId in if let strongSelf = self { - strongSelf.controllerInteraction?.activateAdAction(messageId, nil) + strongSelf.controllerInteraction?.activateAdAction(messageId, nil, true, true) } }, addContact: { [weak self] phoneNumber in if let strongSelf = self { @@ -2374,7 +2412,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let botPeer = botPeer { let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { value in + |> deliverOnMainQueue).startStandalone(next: { [weak self] value in guard let strongSelf = self else { return } @@ -2382,12 +2420,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if value { openBot() } else { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_BotInteractiveUrlAlert(EnginePeer(botPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(botPeer), completion: { [weak self] _ in if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openBot() } - })]), in: .window(.root), with: nil) + openBot() + }, showMore: nil, openTerms: { [weak self] in + if let self, let navigationController = self.effectiveNavigationController { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + }) + strongSelf.present(controller, in: .window(.root)) } }) } @@ -2610,7 +2653,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let message, let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute { - strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId) + strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: false, fullscreen: false) } if let performOpenURL = strongSelf.performOpenURL { @@ -3812,8 +3855,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { storeMessageTextInPasteboard(text, entities: nil) + var infoText = presentationData.strings.Conversation_TextCopied + if let peerId = strongSelf.chatLocation.peerId, peerId.isVerificationCodes && text.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil { + infoText = presentationData.strings.Conversation_CodeCopied + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: infoText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return true }), in: .current) } @@ -3924,8 +3972,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source) - }, activateAdAction: { [weak self] messageId, progress in - guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else { + }, activateAdAction: { [weak self] messageId, progress, media, fullscreen in + guard let self else { + return + } + + var message: Message? + if let historyMessage = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + message = historyMessage + } else if let panelMessage = self.chatDisplayNode.adPanelNode?.message, panelMessage.id == messageId { + message = panelMessage + } + + guard let message, let adAttribute = message.adAttribute else { return } @@ -3938,8 +3997,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId) + self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: media, fullscreen: fullscreen) self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress)) + }, adContextAction: { [weak self] message, sourceNode, gesture in + guard let self else { + return + } + var isBot = false + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + isBot = true + } + let controller = AdsInfoScreen( + context: context, + mode: isBot ? .bot : .channel, + message: message + ) + controller.removeAd = { [weak self] opaqueId in + self?.removeAd(opaqueId: opaqueId) + } + self.effectiveNavigationController?.pushViewController(controller) + }, removeAd: { [weak self] opaqueId in + guard let self else { + return + } + self.removeAd(opaqueId: opaqueId) }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in guard let self else { return @@ -4260,6 +4341,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G adOpaqueId = adAttribute.opaqueId } } + if adOpaqueId == nil, let panelMessage = self.chatDisplayNode.adPanelNode?.message, let adAttribute = panelMessage.adAttribute { + adOpaqueId = adAttribute.opaqueId + } let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start() if let adOpaqueId { self.removeAd(opaqueId: adOpaqueId) @@ -4280,7 +4364,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - self.push(AdsInfoScreen(context: self.context)) + self.push(AdsInfoScreen(context: self.context, mode: .channel)) }, displayGiveawayParticipationStatus: { [weak self] messageId in guard let self else { return @@ -4639,6 +4723,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.chatDisplayNode.forceUpdateWarpContents() + }, playShakeAnimation: { [weak self] in + guard let self else { + return + } + self.playShakeAnimation() }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -5404,30 +5493,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if selectionState.selectedIds.count > 0 { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false) } else { - if let reportReason = presentationInterfaceState.reportReason { - let title: String - switch reportReason { - case .spam: - title = presentationInterfaceState.strings.ReportPeer_ReasonSpam - case .fake: - title = presentationInterfaceState.strings.ReportPeer_ReasonFake - case .violence: - title = presentationInterfaceState.strings.ReportPeer_ReasonViolence - case .porno: - title = presentationInterfaceState.strings.ReportPeer_ReasonPornography - case .childAbuse: - title = presentationInterfaceState.strings.ReportPeer_ReasonChildAbuse - case .copyright: - title = presentationInterfaceState.strings.ReportPeer_ReasonCopyright - case .illegalDrugs: - title = presentationInterfaceState.strings.ReportPeer_ReasonIllegalDrugs - case .personalDetails: - title = presentationInterfaceState.strings.ReportPeer_ReasonPersonalDetails - case .custom: - title = presentationInterfaceState.strings.ReportPeer_ReasonOther - case .irrelevantLocation: - title = "" - } + if let (title, _, _) = presentationInterfaceState.reportReason { strongSelf.chatTitleView?.titleContent = .custom(title, presentationInterfaceState.strings.Conversation_SelectMessages, false) } else { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectMessages, nil, false) @@ -5529,6 +5595,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isPremiumRequiredForMessaging = .single(false) } + let adMessage: Signal + if let adMessagesContext = self.chatDisplayNode.historyNode.adMessagesContext { + adMessage = adMessagesContext.state |> map { $0.messages.first } + } else { + adMessage = .single(nil) + } + self.peerDisposable.set(combineLatest( queue: Queue.mainQueue(), peerView.get(), @@ -5541,10 +5614,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, - managingBot - ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in + managingBot, + adMessage + ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, adMessage in if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot { + if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot && adMessage?.id == strongSelf.presentationInterfaceState.adMessage?.id { return } @@ -5829,54 +5903,75 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G boostsToUnrestrict = cachedChannelData.boostsToUnrestrict } + var adMessage = adMessage + if let peer = peerView.peers[peerView.peerId] as? TelegramUser, peer.botInfo != nil { + } else { + adMessage = nil + } + + if strongSelf.presentationInterfaceState.adMessage?.id != adMessage?.id { + animated = true + } + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer - }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedBotMenuButton(botMenuButton).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages) - .updatedAutoremoveTimeout(autoremoveTimeout) - .updatedCurrentSendAsPeerId(currentSendAsPeerId) - .updatedCopyProtectionEnabled(copyProtectionEnabled) - .updatedHasSearchTags(hasSearchTags) - .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) - .updatedHasSavedChats(hasSavedChats) - .updatedAppliedBoosts(appliedBoosts) - .updatedBoostsToUnrestrict(boostsToUnrestrict) - .updatedHasBirthdayToday(hasBirthdayToday) - .updatedBusinessIntro(businessIntro) - .updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - - if let channel = renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } else if let group = renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } - - return interfaceState - } + }.updatedIsNotAccessible(isNotAccessible) + .updatedContactStatus(contactStatus) + .updatedHasBots(hasBots) + .updatedHasBotCommands(hasBotCommands) + .updatedBotMenuButton(botMenuButton) + .updatedIsArchived(isArchived) + .updatedPeerIsMuted(peerIsMuted) + .updatedPeerDiscussionId(peerDiscussionId) + .updatedPeerGeoLocation(peerGeoLocation) + .updatedExplicitelyCanPinMessages(explicitelyCanPinMessages) + .updatedHasScheduledMessages(hasScheduledMessages) + .updatedAutoremoveTimeout(autoremoveTimeout) + .updatedCurrentSendAsPeerId(currentSendAsPeerId) + .updatedCopyProtectionEnabled(copyProtectionEnabled) + .updatedHasSearchTags(hasSearchTags) + .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) + .updatedHasSavedChats(hasSavedChats) + .updatedAppliedBoosts(appliedBoosts) + .updatedBoostsToUnrestrict(boostsToUnrestrict) + .updatedHasBirthdayToday(hasBirthdayToday) + .updatedBusinessIntro(businessIntro) + .updatedAdMessage(adMessage) + .updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + + if let channel = renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } else if let group = renderedPeer?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } + + return interfaceState + } }) - - if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { + // MARK: Nicegram NCG-6373 Feed tab, !isFeed + if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info, !isFeed { var isRegularChat = false if let subject = subject { if case .message = subject { @@ -6984,12 +7079,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), self.disableStickerAnimationsPromise.get()).startStrict(next: { [weak self] sharedData, disableStickerAnimations in + self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), + self.disableStickerAnimationsPromise.get(), + context.sharedContext.hasGroupCallOnScreen + ).startStrict(next: { [weak self] sharedData, disableStickerAnimations, hasGroupCallOnScreen in var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value } + var disableStickerAnimations = disableStickerAnimations + if hasGroupCallOnScreen { + disableStickerAnimations = true + } + let chatStickerSettings = ChatInterfaceStickerSettings(stickerSettings: stickerSettings) if let strongSelf = self, strongSelf.stickerSettings != chatStickerSettings || strongSelf.disableStickerAnimationsValue != disableStickerAnimations { strongSelf.stickerSettings = chatStickerSettings @@ -7059,6 +7163,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return state.updatedInterfaceState({ $0.withUpdatedSelectedMessages(messageIds) }) }) } + + // MARK: Nicegram + self.nicegramCloseCallback = TgChatCloseCallback.callback + TgChatCloseCallback.callback = nil + // } required public init(coder aDecoder: NSCoder) { @@ -7066,6 +7175,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } deinit { + // MARK: Nicegram + self.nicegramCloseCallback?() + // + let _ = ChatControllerCount.modify { value in return value - 1 } @@ -7162,6 +7275,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.recorderDataDisposable.dispose() self.displaySendWhenOnlineTipDisposable.dispose() self.networkSpeedEventsDisposable?.dispose() + self.postedScheduledMessagesEventsDisposable?.dispose() } deallocate() } @@ -7846,10 +7960,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } }) - } else if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 { + } else if peerId.isTelegramNotifications { self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in if let strongSelf = self, strongSelf.traceVisibility() { - let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) + let loginCodeRegex = try? NSRegularExpression(pattern: "\\b\\d{5,7}\\b", options: []) var loginCodesToInvalidate: [String] = [] strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in if let text = itemNode.item?.message.text, let matches = loginCodeRegex?.matches(in: text, options: [], range: NSMakeRange(0, (text as NSString).length)), let match = matches.first { @@ -8529,7 +8643,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities) var state = state - if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { + if let editMessageState = state.editMessageState { state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference)) } if !text.isEmpty { @@ -9144,6 +9258,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + func shouldDivertMessagesToScheduled(targetPeer: EnginePeer? = nil, messages: [EnqueueMessage]) -> Signal { + return .single(false) + } + func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { if case let .customChatContents(customChatContents) = self.subject { customChatContents.enqueueMessages(messages: messages) @@ -9154,37 +9272,78 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - if commit || !isScheduledMessages { - self.commitPurposefulAction() + let _ = (self.shouldDivertMessagesToScheduled(messages: messages) + |> deliverOnMainQueue).startStandalone(next: { [weak self] shouldDivert in + guard let self else { + return + } - let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + var messages = messages + var shouldOpenScheduledMessages = false + + if shouldDivert { + messages = messages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } } - }) + shouldOpenScheduledMessages = true + } - donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } - self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) - } else { - self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in - if let strongSelf = self { - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) + if commit || !isScheduledMessages { + self.commitPurposefulAction() + + let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }) + + donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + + self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) + + if !isScheduledMessages && shouldOpenScheduledMessages { + if let layoutActionOnViewTransitionAction = self.layoutActionOnViewTransitionAction { + self.layoutActionOnViewTransitionAction = nil + layoutActionOnViewTransitionAction() + } + + self.openScheduledMessages(force: true, completion: { _ in + }) } - }) - } + } else { + self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in + if let strongSelf = self { + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) + } + }) + } + }) } func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] items in - if let strongSelf = self { + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.shouldDivertMessagesToScheduled(messages: items.map(\.message)) + |> deliverOnMainQueue).startStandalone(next: { shouldDivert in + guard let strongSelf = self else { + return + } + var completionImpl: (() -> Void)? = completion var usedCorrelationId: Int64? @@ -9196,6 +9355,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var skipAddingTransitions = false + if shouldDivert { + skipAddingTransitions = true + } + for item in items { var message = item.message if message.groupingKey != nil { @@ -9319,7 +9482,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = scheduleTime { completion() } - } + }) })) } @@ -9639,13 +9802,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.resolvePeerByNameDisposable?.set((resolveSignal |> deliverOnMainQueue).start(next: { [weak self] peer in if let self, !hashtag.isEmpty { + if let _ = peerName, peer == nil { + self.present(textAlertController(context: self.context, title: nil, text: self.presentationInterfaceState.strings.Resolve_ChannelErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: self.presentationInterfaceState.strings.Common_OK, action: {})]), in: .window(.root)) + return + } var publicPosts = false if let peer = self.presentationInterfaceState.renderedPeer, let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, !(channel.addressName ?? "").isEmpty { publicPosts = true } else if case let .customChatContents(contents) = self.subject, case let .hashTagSearch(publicPostsValue) = contents.kind { publicPosts = publicPostsValue } - let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, publicPosts: publicPosts) + let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: peerName == nil && publicPosts) self.effectiveNavigationController?.pushViewController(searchController) } })) @@ -9767,7 +9934,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { urlContext = .generic } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, forceUpdate: false, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return } @@ -9838,7 +10005,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G commit() }) } else { - strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload) commit() } } @@ -9871,7 +10038,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, contentContext: nil, progress: progress, completion: nil) } - func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise? = nil, commit: @escaping () -> Void = {}) { + func openUrl( + _ url: String, + concealed: Bool, + forceExternal: Bool = false, + forceUpdate: Bool = false, + skipUrlAuth: Bool = false, + skipConcealedAlert: Bool = false, + message: Message? = nil, + allowInlineWebpageResolution: Bool = false, + progress: Promise? = nil, + commit: @escaping () -> Void = {} + ) { self.commitPurposefulAction() if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url { @@ -9981,8 +10159,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.interfaceInteraction?.beginMessageSearch(.everything, query) } - public func beginReportSelection(reason: ReportReason) { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedReportReason(reason).updatedInterfaceState { $0.withUpdatedSelectedMessages([]) } }) + public func beginReportSelection(reason: NavigateToChatControllerParams.ReportReason) { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedReportReason((reason.title, reason.option, reason.message)).updatedInterfaceState { $0.withUpdatedSelectedMessages([]) } }) } func displayMediaRecordingTooltip() { @@ -10297,6 +10475,60 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + func displayProcessingVideoTooltip(messageId: EngineMessage.Id) { + self.checksTooltipController?.dismiss() + + var latestNode: ChatMessageItemView? + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + var found = false + for (message, _) in item.content { + if message.id == messageId { + found = true + break + } + } + if !found { + return + } + latestNode = itemNode + } + } + + if let itemNode = latestNode, let statusNode = itemNode.getStatusNode() { + let bounds = statusNode.view.convert(statusNode.view.bounds, to: self.chatDisplayNode.view) + let location = CGPoint(x: bounds.midX, y: bounds.minY - 8.0) + + let tooltipController = TooltipController(content: .text(self.presentationData.strings.Chat_MessageTooltipVideoProcessing), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, isBlurred: true, timeout: 4.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + self.checksTooltipController = tooltipController + tooltipController.dismissed = { [weak self, weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController { + strongSelf.checksTooltipController = nil + } + } + + let _ = self.chatDisplayNode.messageTransitionNode.addCustomOffsetHandler(itemNode: itemNode, update: { [weak tooltipController] offset, transition in + guard let tooltipController, tooltipController.isNodeLoaded else { + return false + } + guard let containerView = tooltipController.view else { + return false + } + containerView.bounds = containerView.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(layer: containerView.layer, offset: offset) + + return true + }) + + self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + guard let self else { + return nil + } + return (self.chatDisplayNode, CGRect(origin: location, size: CGSize())) + }, sourceRectIsGlobal: true)) + } + } + func dismissAllTooltips() { self.emojiTooltipController?.dismiss() self.sendingOptionsTooltipController?.dismiss() @@ -10460,8 +10692,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func openScheduledMessages() { - guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else { + func openScheduledMessages(force: Bool = false, completion: @escaping (ChatControllerImpl) -> Void = { _ in }) { + guard let navigationController = self.effectiveNavigationController else { + return + } + if navigationController.topViewController == self || force { + } else { return } @@ -10472,7 +10708,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages) controller.navigationPresentation = .modal - navigationController.pushViewController(controller) + navigationController.pushViewController(controller, completion: { [weak controller] in + let _ = controller + /*if let controller { + completion(controller) + }*/ + }) + completion(controller) } func openPinnedMessages(at messageId: MessageId?) { @@ -10920,4 +11162,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } } + + public var contentContainerNode: ASDisplayNode { + return self.chatDisplayNode.contentContainerNode + } } diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 6996612b05e..f103ba74d80 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -174,6 +174,9 @@ extension ChatControllerImpl { }) let commit: ([EnqueueMessage]) -> Void = { result in + guard let strongSelf = self else { + return + } var result = result strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) @@ -185,96 +188,128 @@ extension ChatControllerImpl { result[i] = result[i].withUpdatedCorrelationId(correlationId) } - var displayPeers: [EnginePeer] = [] - for peer in peers { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { - let signals: [Signal] = messageIds.compactMap({ id -> Signal? in - guard let id = id else { - return nil + let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in + return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) + |> map { shouldDivert -> (EnginePeer, Bool) in + return (peer, shouldDivert) + } + } + let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) + let _ = (targetPeersShouldDivert + |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in + guard let strongSelf = self else { + return + } + + var displayConvertingTooltip = false + + var displayPeers: [EnginePeer] = [] + for (peer, shouldDivert) in targetPeersShouldDivert { + var peerMessages = result + if shouldDivert { + displayConvertingTooltip = true + peerMessages = peerMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes } - return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) - |> mapToSignal { status, _ -> Signal in - if status != nil { - return .never() - } else { - return .single(true) + } + } + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() } - |> take(1) - }) - if strongSelf.shareStatusDisposable == nil { - strongSelf.shareStatusDisposable = MetaDisposable() + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict()) } - strongSelf.shareStatusDisposable?.set((combineLatest(signals) - |> deliverOnMainQueue).startStrict()) - } - }) - - if case let .secretChat(secretPeer) = peer { - if let peer = peerMap[secretPeer.regularPeerId] { + }) + + if case let .secretChat(secretPeer) = peer { + if let peer = peerMap[secretPeer.regularPeerId] { + displayPeers.append(peer) + } + } else { displayPeers.append(peer) } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true } else { - displayPeers.append(peer) + if displayPeers.count == 1, let peer = displayPeers.first { + var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string + } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { + var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string + } else if let peer = displayPeers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string + } else { + text = "" + } } - } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many - savedMessages = true - } else { - if displayPeers.count == 1, let peer = displayPeers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string - } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string - } else if let peer = displayPeers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string + let reactionItems: Signal<[ReactionItem], NoError> + if savedMessages && messages.count > 0 { + reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) } else { - text = "" - } - } - - let reactionItems: Signal<[ReactionItem], NoError> - if savedMessages && messages.count > 0 { - reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) - } else { - reactionItems = .single([]) - } - - let _ = (reactionItems - |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in - guard let strongSelf else { - return + reactionItems = .single([]) } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) + let _ = (reactionItems + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in + guard let strongSelf else { + return } - return false - }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + }) + + if displayConvertingTooltip { + } }) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index b272a50dfba..2f5f7e987f2 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -216,6 +216,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var chatImportStatusPanel: ChatImportStatusPanel? + private(set) var adPanelNode: ChatAdPanelNode? + private let titleAccessoryPanelContainer: ChatControllerTitlePanelNodeContainer private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode? @@ -444,8 +446,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var displayVideoUnmuteTipDisposable: Disposable? private var onLayoutCompletions: [(ContainedViewLayoutTransition) -> Void] = [] - - init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, statusBar: StatusBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?) { +// MARK: Nicegram NCG-6373 Feed tab + private let isFeed: Bool + // MARK: Nicegram NCG-6373 Feed tab, isFeed + init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, statusBar: StatusBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?, isFeed: Bool) { +// MARK: Nicegram NCG-6373 Feed tab + self.isFeed = isFeed +// self.context = context self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction @@ -888,7 +895,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.wrappingNode.contentNode.addSubnode(self.inputContextPanelContainer) - self.wrappingNode.contentNode.addSubnode(self.inputPanelContainerNode) +// MARK: Nicegram NCG-6373 Feed tab + if !isFeed { + self.wrappingNode.contentNode.addSubnode(self.inputPanelContainerNode) + } +// self.wrappingNode.contentNode.addSubnode(self.inputContextOverTextPanelContainer) self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) @@ -1298,7 +1309,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true if self.historyNodeContainer.isSecret != isSecret { self.historyNodeContainer.isSecret = isSecret setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) @@ -1615,6 +1626,50 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.chatImportStatusPanel = nil } + var dismissedAdPanelNode: ChatAdPanelNode? + var adPanelHeight: CGFloat? + + var displayAdPanel = false + if let _ = self.chatPresentationInterfaceState.adMessage { + if let chatHistoryState = self.chatPresentationInterfaceState.chatHistoryState, case .loaded(false, _) = chatHistoryState { + if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil && !self.chatPresentationInterfaceState.peerIsBlocked && self.chatPresentationInterfaceState.hasAtLeast3Messages { + displayAdPanel = true + } + } + } + + if displayAdPanel { + var animateAppearance = false + let adPanelNode: ChatAdPanelNode + if let current = self.adPanelNode { + adPanelNode = current + } else { + adPanelNode = ChatAdPanelNode(context: self.context, animationCache: self.controllerInteraction.presentationContext.animationCache, animationRenderer: self.controllerInteraction.presentationContext.animationRenderer) + adPanelNode.controllerInteraction = self.controllerInteraction + adPanelNode.clipsToBounds = true + animateAppearance = true + } + + if self.adPanelNode != adPanelNode { + dismissedAdPanelNode = self.adPanelNode + self.adPanelNode = adPanelNode + self.titleAccessoryPanelContainer.addSubnode(adPanelNode) + + adPanelNode.clipsToBounds = true + } + + let height = adPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) + adPanelHeight = height + if transition.isAnimated && animateAppearance { + adPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + adPanelNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -height, 0.0) + transition.updateSublayerTransformOffset(layer: adPanelNode.layer, offset: CGPoint()) + } + } else if let adPanelNode = self.adPanelNode { + dismissedAdPanelNode = adPanelNode + self.adPanelNode = nil + } + var inputPanelNodeBaseHeight: CGFloat = 0.0 if let inputPanelNode = self.inputPanelNode { inputPanelNodeBaseHeight += inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) @@ -1914,14 +1969,21 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { extraNavigationBarHeight += panelHeight } - updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraTransition) - var importStatusPanelFrame: CGRect? if let _ = self.chatImportStatusPanel, let panelHeight = importStatusPanelHeight { importStatusPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight } + var adPanelFrame: CGRect? + if let _ = self.adPanelNode, let panelHeight = adPanelHeight { + adPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: extraNavigationBarHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + insets.top += panelHeight + extraNavigationBarHeight += panelHeight + } + + updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraTransition) + let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) if let backgroundEffectNode = self.backgroundEffectNode { @@ -2060,7 +2122,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { inputPanelsHeight = 0.0 } } - +// MARK: Nicegram NCG-6373 Feed tab + if isFeed { + inputPanelsHeight = 0.0 + } +// let inputBackgroundInset: CGFloat if cleanInsets.bottom < insets.bottom { if case .regular = layout.metrics.widthClass, insets.bottom < 88.0 { @@ -2383,21 +2449,25 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { ) // - if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { + if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { titleAccessoryPanelNode.frame = titleAccessoryPanelFrame transition.animatePositionAdditive(node: titleAccessoryPanelNode, offset: CGPoint(x: 0.0, y: -titleAccessoryPanelFrame.height)) } - if let chatTranslationPanel = self.chatTranslationPanel, let translationPanelFrame = translationPanelFrame, !chatTranslationPanel.frame.equalTo(translationPanelFrame) { + if let chatTranslationPanel = self.chatTranslationPanel, let translationPanelFrame, !chatTranslationPanel.frame.equalTo(translationPanelFrame) { chatTranslationPanel.frame = translationPanelFrame transition.animatePositionAdditive(node: chatTranslationPanel, offset: CGPoint(x: 0.0, y: -translationPanelFrame.height)) } - if let chatImportStatusPanel = self.chatImportStatusPanel, let importStatusPanelFrame = importStatusPanelFrame, !chatImportStatusPanel.frame.equalTo(importStatusPanelFrame) { + if let chatImportStatusPanel = self.chatImportStatusPanel, let importStatusPanelFrame, !chatImportStatusPanel.frame.equalTo(importStatusPanelFrame) { chatImportStatusPanel.frame = importStatusPanelFrame //transition.animatePositionAdditive(node: chatImportStatusPanel, offset: CGPoint(x: 0.0, y: -titleAccessoryPanelFrame.height)) } + if let adPanelNode = self.adPanelNode, let adPanelFrame, !adPanelNode.frame.equalTo(adPanelFrame) { + adPanelNode.frame = adPanelFrame + } + if let secondaryInputPanelNode = self.secondaryInputPanelNode, let apparentSecondaryInputPanelFrame = apparentSecondaryInputPanelFrame, !secondaryInputPanelNode.frame.equalTo(apparentSecondaryInputPanelFrame) { if immediatelyLayoutSecondaryInputPanelAndAnimateAppearance { secondaryInputPanelNode.frame = apparentSecondaryInputPanelFrame.offsetBy(dx: 0.0, dy: apparentSecondaryInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentSecondaryInputPanelFrame.maxY) @@ -2489,7 +2559,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if let dismissedTitleAccessoryPanelNode = dismissedTitleAccessoryPanelNode { + if let dismissedTitleAccessoryPanelNode { var dismissedPanelFrame = dismissedTitleAccessoryPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateFrame(node: dismissedTitleAccessoryPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedTitleAccessoryPanelNode] _ in @@ -2497,7 +2567,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }) } - if let dismissedTranslationPanelNode = dismissedTranslationPanelNode { + if let dismissedTranslationPanelNode { var dismissedPanelFrame = dismissedTranslationPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateAlpha(node: dismissedTranslationPanelNode, alpha: 0.0, completion: { [weak dismissedTranslationPanelNode] _ in @@ -2506,7 +2576,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { dismissedTranslationPanelNode.animateOut() } - if let dismissedImportStatusPanelNode = dismissedImportStatusPanelNode { + if let dismissedImportStatusPanelNode { var dismissedPanelFrame = dismissedImportStatusPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateFrame(node: dismissedImportStatusPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedImportStatusPanelNode] _ in @@ -2514,6 +2584,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }) } + if let dismissedAdPanelNode { + var dismissedPanelFrame = dismissedAdPanelNode.frame + dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height + transition.updateAlpha(node: dismissedAdPanelNode, alpha: 0.0) + transition.updateFrame(node: dismissedAdPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedAdPanelNode] _ in + dismissedAdPanelNode?.removeFromSupernode() + }) + } + if let inputPanelNode = self.inputPanelNode, let apparentInputPanelFrame = apparentInputPanelFrame, !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { if immediatelyLayoutInputPanelAndAnimateAppearance { inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentInputBackgroundFrame.maxY) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index f8d76a3a32f..a248e12346d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -588,19 +588,17 @@ extension ChatControllerImpl { strongSelf.controllerNavigationDisposable.set(nil) } case .gift: - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in - if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) + let controller = PremiumGiftAttachmentScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumGiftOptions, completion: { [weak self] in + guard let self else { + return } + self.hintPlayNextOutgoingGift() + self.attachmentController?.dismiss(animated: true) }) + completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) @@ -618,8 +616,8 @@ extension ChatControllerImpl { let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) - controller.openUrl = { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + controller.openUrl = { [weak self] url, concealed, forceUpdate, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, forceUpdate: forceUpdate, commit: commit) } controller.getNavigationController = { [weak self] in return self?.effectiveNavigationController @@ -713,7 +711,7 @@ extension ChatControllerImpl { }) } - func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { + func presentEditingAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -816,188 +814,184 @@ extension ChatControllerImpl { hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat } - let controller = legacyAttachmentMenu(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: hasSchedule, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { - self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { + let controller = legacyAttachmentMenu( + context: strongSelf.context, + peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, + threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, + editMediaOptions: menuEditMediaOptions, + addingMedia: editMediaOptions == nil, + saveEditedPhotos: settings.storeEditedPhotos, + allowGrouping: true, + hasSchedule: hasSchedule, + canSendPolls: canSendPolls, + updatedPresentationData: strongSelf.updatedPresentationData, + parentController: legacyController, + recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, + initialCaption: inputText, + openGallery: { + self?.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, silentPosting, scheduleTime in + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } self?.editMessageMediaWithLegacySignals(signals) - } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - }) - }, openCamera: { [weak self] cameraView, menuController in - if let strongSelf = self { - var enablePhoto = true - var enableVideo = true - - if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - enableVideo = false - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) + }) + }, openCamera: { [weak self] cameraView, menuController in + if let strongSelf = self { + var enablePhoto = true + var enableVideo = true + + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } } } - } - - if bannedSendPhotos != nil { - enablePhoto = false - } - if bannedSendVideos != nil { - enableVideo = false - } - - var storeCapturedPhotos = false - var hasSchedule = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat - } - - presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in - if let strongSelf = self { - if editMediaOptions != nil { + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + var storeCapturedPhotos = false + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat + + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + } + + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + if let strongSelf = self { strongSelf.editMessageMediaWithLegacySignals(signals!) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } } - if !inputText.string.isEmpty { - strongSelf.clearInputText() + }, recognizedQRCode: { [weak self] code in + if let strongSelf = self { + if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + } } - } - }, recognizedQRCode: { [weak self] code in - if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { - strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) } - } - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }) - } - }, openFileGallery: { - self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) - }, openMap: { - self?.presentLocationPicker() - }, openContacts: { - self?.presentContactPicker() - }, openPoll: { - if let controller = self?.configurePollCreation() { - self?.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { - guard let strongSelf = self else { - return - } - let text: String - if slowModeEnabled { - text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached - } else { - text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentCantSendMultipleFiles: { - guard let strongSelf = self else { - return - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentJpegConversionAlert: { completion in - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { - completion(false) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { - completion(true) - })], actionLayout: .vertical), in: .window(.root)) - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() + }) } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - guard let strongSelf = self else { - completion() - return - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { - strongSelf.editMessageMediaWithLegacySignals(signals!) - completion() - } else { - let immediateCompletion = getAnimatedTransitionSource == nil - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { - if !immediateCompletion { - completion() - } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }) + } + }, openFileGallery: { + self?.presentFileMediaPickerOptions(editingMessage: true) + }, openWebSearch: { [weak self] in + self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) }) - if immediateCompletion { + }, openMap: { + self?.presentLocationPicker() + }, openContacts: { + self?.presentContactPicker() + }, openPoll: { + if let controller = self?.configurePollCreation() { + self?.effectiveNavigationController?.pushViewController(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentCantSendMultipleFiles: { + guard let strongSelf = self else { + return + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentJpegConversionAlert: { completion in + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { + completion(false) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { + completion(true) + })], actionLayout: .vertical), in: .window(.root)) + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let strongSelf = self else { completion() + return } - } - }, selectRecentlyUsedInlineBot: { [weak self] peer in - if let strongSelf = self, let addressName = peer.addressName { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in - return .text + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + strongSelf.editMessageMediaWithLegacySignals(signals!) + completion() + }, selectRecentlyUsedInlineBot: { [weak self] peer in + if let strongSelf = self, let addressName = peer.addressName { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in + return .text + }) }) - }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + ) controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() } @@ -1109,7 +1103,7 @@ extension ChatControllerImpl { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 7ca0a4a4014..ee04acb0df8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -192,7 +192,7 @@ extension ChatControllerImpl { if canViewMessageReactionList(message: message) { items = ContextController.Items(content: .custom(ReactionListContextMenuContent( context: self.context, - displayReadTimestamps: false, + displayReadTimestamps: true, availableReactions: availableReactions, animationCache: self.controllerInteraction!.presentationContext.animationCache, animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, @@ -513,7 +513,7 @@ extension ChatControllerImpl { title = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount)) } - let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ + let textItems = AnimatedTextComponent.extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ 0: .number(self.currentSendStarsUndoCount, minDigits: 1), 1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount))) ]) @@ -536,31 +536,3 @@ extension ChatControllerImpl { } } } - -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} diff --git a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift index 59f3755db38..07cd1d573dc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift @@ -77,6 +77,7 @@ extension ChatControllerImpl { if queryIsEmpty { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) self.searchResult.set(.single(nil)) if let data = interfaceState.search { @@ -104,6 +105,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { @@ -152,6 +154,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let previousResultsState = data.resultsState { let messageIndices = results.messages.map({ $0.index }).sorted() @@ -167,11 +170,13 @@ extension ChatControllerImpl { })) } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) } } } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) if let data = interfaceState.search { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 0c8f47b7581..bff0145832e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -223,23 +223,6 @@ func chatHistoryEntriesForView( } if groupMessages || reverseGroupedMessages { - /*if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { - if reverseGroupedMessages { - groupBucket.reverse() - } - if groupMessages { - let groupStableId = groupBucket[0].0.groupInfo!.stableId - if !existingGroupStableIds.contains(groupStableId) { - existingGroupStableIds.append(groupStableId) - entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) - } - } else { - for (message, isRead, selection, attributes, location) in groupBucket { - entries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) - } - } - groupBucket.removeAll() - }*/ if let messageGroupingKey = message.groupingKey, (groupMessages || reverseGroupedMessages) { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -286,14 +269,6 @@ func chatHistoryEntriesForView( if !found { entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, attributes)) } - - /*let selection: ChatHistoryMessageSelection - if let selectedMessages = selectedMessages { - selection = .selectable(selected: selectedMessages.contains(message.id)) - } else { - selection = .none - } - groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), entry.location))*/ } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -316,27 +291,63 @@ func chatHistoryEntriesForView( } else { selection = .none } + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) } } - - /*if !groupBucket.isEmpty { - assert(groupMessages || reverseGroupedMessages) - if reverseGroupedMessages { - groupBucket.reverse() - } - if groupMessages { - let groupStableId = groupBucket[0].0.groupInfo!.stableId - if !existingGroupStableIds.contains(groupStableId) { - existingGroupStableIds.append(groupStableId) - entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) + + let insertPendingProcessingMessage: ([Message], Int) -> Void = { messages, index in + let serviceMessage = Message( + stableId: UInt32.max - messages[0].stableId, + stableVersion: 0, + id: MessageId(peerId: messages[0].id.peerId, namespace: -1, id: messages[0].id.id), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: messages[0].timestamp, + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Chat_VideoProcessingServiceMessage(Int32(messages.count)), entities: [], additionalAttributes: nil))], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: index) + } + + for i in (0 ..< entries.count).reversed() { + switch entries[i] { + case let .MessageEntry(message, _, _, _, _, _): + if message.id.namespace == Namespaces.Message.ScheduledCloud && message.pendingProcessingAttribute != nil { + insertPendingProcessingMessage([message], i) } - } else { - for (message, isRead, selection, attributes, location) in groupBucket { - entries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) + case let .MessageGroupEntry(_, messages, _): + if !messages.isEmpty && messages[0].0.id.namespace == Namespaces.Message.ScheduledCloud { + var videoCount = 0 + for message in messages { + if message.0.pendingProcessingAttribute != nil { + videoCount += 1 + } + } + if videoCount != 0 { + insertPendingProcessingMessage(messages.map(\.0), i) + } } + default: + break } - }*/ + } if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp { if let joinMessage { @@ -446,6 +457,8 @@ func chatHistoryEntriesForView( } if case let .peer(peerId) = location, peerId.isReplies { entries.insert(.ChatInfoEntry("", presentationData.strings.RepliesChat_DescriptionText, nil, nil, presentationData), at: 0) + } else if case let .peer(peerId) = location, peerId.isVerificationCodes { + entries.insert(.ChatInfoEntry("", presentationData.strings.VerificationCodes_DescriptionText, nil, nil, presentationData), at: 0) } else if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { entries.insert(.ChatInfoEntry(presentationData.strings.Bot_DescriptionTitle, botInfo.description, botInfo.photo, botInfo.video, presentationData), at: 0) } else { @@ -655,6 +668,9 @@ func chatHistoryEntriesForView( if reverse { return (entries.reversed(), currentState) } else { + #if DEBUG + assert(entries.map(\.stableId) == entries.sorted().map(\.stableId)) + #endif return (entries, currentState) } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3682a6c75ee..1441e3cfb03 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1,3 +1,7 @@ +// MARK: Nicegram ATT +import ChatMessageNicegramAdNode +import FeatAttentionEconomy +// // MARK: Nicegram AiChat import NGAiChatUI // @@ -276,6 +280,14 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() }), directionHint: entry.directionHint) + // MARK: Nicegram ATT + case let .NicegramAdEntry(_, ad, presentationData): + if #available(iOS 15.0, *) { + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatMessageNicegramAdItem(ad: ad, chatLocation: chatLocation, context: context, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) + } else { + fatalError() + } + // } } } @@ -328,6 +340,14 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() }), directionHint: entry.directionHint) + // MARK: Nicegram ATT + case let .NicegramAdEntry(_, ad, presentationData): + if #available(iOS 15.0, *) { + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatMessageNicegramAdItem(ad: ad, chatLocation: chatLocation, context: context, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) + } else { + fatalError() + } + // } } } @@ -678,6 +698,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? private var additionalLoadStateUpdated: [(ChatHistoryNodeLoadState, Bool) -> Void] = [] + public private(set) var hasAtLeast3Messages: Bool = false + public var hasAtLeast3MessagesUpdated: ((Bool) -> Void)? + public private(set) var hasPlentyOfMessages: Bool = false public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)? @@ -808,7 +831,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if case .bubbles = mode, let peerId = displayAdPeer { let adMessagesContext = context.engine.messages.adMessages(peerId: peerId) self.adMessagesContext = adMessagesContext - adMessages = adMessagesContext.state + if peerId.namespace == Namespaces.Peer.CloudUser { + adMessages = .single((nil, [])) + } else { + adMessages = adMessagesContext.state + } } else { self.adMessagesContext = nil adMessages = .single((nil, [])) @@ -1640,6 +1667,27 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } |> distinctUntilChanged + // MARK: Nicegram ATT + let getPlacementAdsUseCase = AttCoreModule.shared.getPlacementAdsUseCase() + let getSettingsUseCase = AttCoreModule.shared.getSettingsUseCase() + let nicegramAd = getPlacementAdsUseCase + .publisher(placementId: .chat) + .combineLatestThreadSafe( + getSettingsUseCase.publisher() + ) + .map { ad, settings in + let enableAds = settings.enableAds + let enablePlacement = settings.settings(for: .chat)?.enabled ?? false + let forceRemove = !enableAds || !enablePlacement + return NicegramAdInChat( + ad: ad, + forceRemove: forceRemove + ) + } + .toSignal() + .skipError() + // + let messageViewQueue = Queue.mainQueue() let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, @@ -1666,8 +1714,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto audioTranscriptionTrial, chatThemes, deviceContactsNumbers, - contentSettings - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in + contentSettings, + // MARK: Nicegram ATT + nicegramAd + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, nicegramAd in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises func applyHole() { @@ -1879,12 +1929,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if languageCode.hasSuffix(rawSuffix) { languageCode = String(languageCode.dropLast(rawSuffix.count)) } - if languageCode == "nb" { - languageCode = "no" - } else if languageCode == "pt-br" { - languageCode = "pt" - } - translateToLanguage = languageCode + translateToLanguage = normalizeTranslationLanguage(languageCode) } let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive")) @@ -1896,7 +1941,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let previousChatHistoryEntriesForViewState = chatHistoryEntriesForViewState.with({ $0 }) - let (filteredEntries, updatedChatHistoryEntriesForViewState) = chatHistoryEntriesForView( + // MARK: Nicegram ATT, changed 'let' to 'var' + var (filteredEntries, updatedChatHistoryEntriesForViewState) = chatHistoryEntriesForView( currentState: previousChatHistoryEntriesForViewState, context: context, location: chatLocation, @@ -1923,6 +1969,26 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto adMessage: allAdMessages.fixed, dynamicAdMessages: allAdMessages.opportunistic ) + // MARK: Nicegram ATT + if let self { + var cachedPeerData: CachedPeerData? + for entry in view.additionalData { + if case let .cachedPeerData(_, maybeCachedPeerData) = entry, let maybeCachedPeerData { + cachedPeerData = maybeCachedPeerData + } + } + + filteredEntries = nicegramMapChatHistoryEntries( + oldEntries: previousView.with { $0?.0.filteredEntries } ?? [], + newEntries: filteredEntries, + nicegramAd: nicegramAd, + visibleItemRange: self.displayedItemRange.visibleRange, + chatPresentationData: chatPresentationData, + cachedPeerData: cachedPeerData + ) + } + // + let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version)) @@ -3500,6 +3566,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } for id in self.context.engine.messages.synchronouslyIsMessageDeletedInteractively(ids: testIds) { + if id.namespace == Namespaces.Message.ScheduledCloud { + continue + } inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds { if listId == id { expiredMessageStableIds.insert(stableId) @@ -3809,13 +3878,18 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + var hasAtLeast3Messages = false var hasPlentyOfMessages = false var hasLotsOfMessages = false if let historyView = strongSelf.historyView { if historyView.originalView.holeEarlier || historyView.originalView.holeLater { + hasAtLeast3Messages = true hasPlentyOfMessages = true hasLotsOfMessages = true } else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater { + if historyView.filteredEntries.count >= 3 { + hasAtLeast3Messages = true + } if historyView.filteredEntries.count >= 10 { hasPlentyOfMessages = true } @@ -3825,6 +3899,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + if strongSelf.hasAtLeast3Messages != hasAtLeast3Messages { + strongSelf.hasAtLeast3Messages = hasAtLeast3Messages + strongSelf.hasAtLeast3MessagesUpdated?(hasAtLeast3Messages) + } if strongSelf.hasPlentyOfMessages != hasPlentyOfMessages { strongSelf.hasPlentyOfMessages = hasPlentyOfMessages strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 60ad36b3211..c8891ef2cf2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -10,7 +10,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult switch result { case let .stickers(items): return (0, !items.isEmpty) - case let .hashtags(items): + case let .hashtags(items, _): return (1, !items.isEmpty) case let .mentions(items): return (2, !items.isEmpty) @@ -25,7 +25,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult case let .emojis(items, _): return (5, !items.isEmpty) // MARK: Nicegram QuickReplies - case let .quickReplies(items): + case let .quickReplies(items, _): return (6, !items.isEmpty) // } @@ -103,28 +103,36 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa } } // MARK: Nicegram QuickReplies - case let .quickReplies(results): + case let .quickReplies(results, query): + var peer: EnginePeer? + if let chatPeer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, chatPeer.addressName != nil { + peer = EnginePeer(chatPeer) + } if !results.isEmpty { if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode { - currentPanel.updateResults(results, canDelete: false) + currentPanel.updateResults(results, query: query, peer: peer, canDelete: false) return currentPanel } else { let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext) panel.interfaceInteraction = interfaceInteraction - panel.updateResults(results, canDelete: false) + panel.updateResults(results, query: query, peer: peer, canDelete: false) return panel } } // - case let .hashtags(results): - if !results.isEmpty { + case let .hashtags(results, query): + var peer: EnginePeer? + if let chatPeer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, chatPeer.addressName != nil { + peer = EnginePeer(chatPeer) + } + if !results.isEmpty || (peer != nil && query.count >= 4) { if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode { - currentPanel.updateResults(results) + currentPanel.updateResults(results, query: query, peer: peer) return currentPanel } else { let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext) panel.interfaceInteraction = interfaceInteraction - panel.updateResults(results) + panel.updateResults(results, query: query, peer: peer) return panel } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 42c0ab9f5f4..0f2f1afab8c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -319,7 +319,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS return false } - guard !peer.id.isReplies else { + guard !peer.id.isRepliesOrVerificationCodes else { return false } switch chatPresentationInterfaceState.mode { @@ -418,7 +418,7 @@ func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions return [] case .Animated: break - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return [] } else { @@ -497,6 +497,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single(ContextController.Items(content: .list([]))) } + if let message = messages.first, message.id.namespace < 0 { + return .single(ContextController.Items(content: .list([]))) + } var isEmbeddedMode = false if case .standard(.embedded) = chatPresentationInterfaceState.mode { @@ -568,7 +571,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { _, f in f(.dismissWithoutContent) - controllerInteraction.navigationController()?.pushViewController(AdsInfoScreen(context: context)) + controllerInteraction.navigationController()?.pushViewController(AdsInfoScreen(context: context, mode: .channel)) }))) actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in @@ -746,7 +749,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.peerId.isReplies { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.peerId.isRepliesOrVerificationCodes { canReply = false canPin = false } else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { @@ -1004,7 +1007,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState strongController.dismiss() let id = Int64.random(in: Int64.min ... Int64.max) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).startStandalone() @@ -1196,9 +1199,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if data.messageActions.options.contains(.sendScheduledNow) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ScheduledMessages_SendNow, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) - f(.dismissWithoutContent) + }, action: { c, _ in + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + c?.dismiss(completion: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + controllerInteraction.presentController(standardTextAlertController( + theme: AlertControllerTheme(presentationData: presentationData), + title: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Title, + text: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Action, action: { + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ), nil) + }) + } else { + c?.dismiss(result: .dismissWithoutContent, completion: nil) + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + } }))) } @@ -2198,6 +2220,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState reactionCount = 0 } } + + let isEdited = message.attributes.contains(where: { attribute in + if let attribute = attribute as? EditedMessageAttribute, !attribute.isHidden, attribute.date != 0 { + return true + } + return false + }) + + if isEdited { + if !actions.isEmpty { + actions.insert(.separator, at: 0) + } + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: false, isEdit: true, stats: MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:]), action: nil), false), at: 0) + } if let peer = message.peers[message.id.peerId], (canViewStats || reactionCount != 0) { var hasReadReports = false @@ -2221,18 +2257,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { reactionCount = 0 } - - /*var readStats = readStats - if !canViewStats { - readStats = MessageReadStats(reactionCount: 0, peers: []) - }*/ if hasReadReports || reactionCount != 0 { if !actions.isEmpty { actions.insert(.separator, at: 0) } + + var readStats = readStats + if !(hasReadReports || reactionCount != 0) { + readStats = MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:]) + } - actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: hasReadReports, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: hasReadReports, isEdit: false, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in if message.id.peerId.namespace == Namespaces.Peer.CloudUser { if let stats, stats.peers.isEmpty { c.dismiss(completion: { @@ -2545,8 +2581,10 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } if id.namespace == Namespaces.Message.ScheduledCloud { optionsMap[id]!.insert(.sendScheduledNow) - if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { - optionsMap[id]!.insert(.editScheduledTime) + if message.pendingProcessingAttribute == nil { + if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { + optionsMap[id]!.insert(.editScheduledTime) + } } if let peer = getPeer(id.peerId), let channel = peer as? TelegramChannel { if !message.flags.contains(.Incoming) { @@ -2645,24 +2683,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer case .creator, .admin: optionsMap[id]!.insert(.deleteGlobally) case .member: - var hasMediaToReport = false - for media in message.media { - if let _ = media as? TelegramMediaImage { - hasMediaToReport = true - } else if let _ = media as? TelegramMediaFile { - hasMediaToReport = true - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let _ = content.image { - hasMediaToReport = true - } else if let _ = content.file { - hasMediaToReport = true - } - } - } - if hasMediaToReport { - optionsMap[id]!.insert(.report) - } + break } + optionsMap[id]!.insert(.report) } } else if let user = peer as? TelegramUser { if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() && !isShareProtected { @@ -2983,13 +3006,15 @@ final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message fileprivate let hasReadReports: Bool + fileprivate let isEdit: Bool fileprivate let stats: MessageReadStats? - fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void + fileprivate let action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void)? - init(context: AccountContext, message: Message, hasReadReports: Bool, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void) { + init(context: AccountContext, message: Message, hasReadReports: Bool, isEdit: Bool, stats: MessageReadStats?, action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void)?) { self.context = context self.message = message self.hasReadReports = hasReadReports + self.isEdit = isEdit self.stats = stats self.action = action } @@ -3068,7 +3093,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording self.iconNode = ASImageNode() - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.isEdit { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuEditIcon"), color: presentationData.theme.actionSheet.primaryTextColor) + } else if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: presentationData.theme.actionSheet.primaryTextColor) } else if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.actionSheet.primaryTextColor) @@ -3167,12 +3194,12 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus if let currentStats = self.currentStats { if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { - self.buttonNode.isUserInteractionEnabled = currentStats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = item.action != nil && currentStats.peers.isEmpty } else { - self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty || reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = item.action != nil && (!currentStats.peers.isEmpty || reactionCount != 0) } } else { - self.buttonNode.isUserInteractionEnabled = reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = item.action != nil && reactionCount != 0 self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id) |> deliverOnMainQueue).startStrict(next: { [weak self] value in @@ -3185,7 +3212,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus }) } - item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) + if !self.item.isEdit { + item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) + } } deinit { @@ -3211,9 +3240,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus func updateStats(stats: MessageReadStats, transition: ContainedViewLayoutTransition) { if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { - self.buttonNode.isUserInteractionEnabled = stats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = self.item.action != nil && stats.peers.isEmpty } else { - self.buttonNode.isUserInteractionEnabled = !stats.peers.isEmpty || stats.reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = self.item.action != nil && (!stats.peers.isEmpty || stats.reactionCount != 0) } guard let (calculatedWidth, size) = self.validLayout else { @@ -3257,7 +3286,24 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus reactionCount = currentStats.reactionCount if currentStats.peers.isEmpty { - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.isEdit, let attribute = self.item.message.attributes.first(where: { $0 is EditedMessageAttribute }) as? EditedMessageAttribute, !attribute.isHidden, attribute.date != 0 { + let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: attribute.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat( + dateFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_Date(value).string, ranges: []) + }, + tomorrowFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_TodayAt(value).string, ranges: []) + }, + todayFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_TodayAt(value).string, ranges: []) + }, + yesterdayFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_YesterdayAt(value).string, ranges: []) + } + )).string + + self.textNode.attributedText = NSAttributedString(string: dateText, font: Font.regular(floor(self.presentationData.listsFontSize.baseDisplaySize * 0.8)), textColor: self.presentationData.theme.contextMenu.primaryColor) + } else if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { let text = NSAttributedString(string: self.presentationData.strings.Chat_ContextMenuReadDate_ReadAvailablePrefix, font: Font.regular(floor(self.presentationData.listsFontSize.baseDisplaySize * 0.8)), textColor: self.presentationData.theme.contextMenu.primaryColor) if self.textNode.attributedText != text { animatePositions = false @@ -3375,7 +3421,12 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let positionTransition: ContainedViewLayoutTransition = animatePositions ? transition : .immediate let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) - let textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize) + var textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize) + + if self.item.isEdit { + textFrame.origin.x -= 2.0 + } + positionTransition.updateFrameAdditive(node: self.textNode, frame: textFrame) transition.updateAlpha(node: self.textNode, alpha: self.currentStats == nil ? 0.0 : 1.0) @@ -3419,14 +3470,18 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus transition.updateAlpha(node: self.shimmerNode, alpha: self.currentStats == nil ? 1.0 : 0.0) if !iconSize.width.isZero { - transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: sideInset + 1.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + var iconFrame = CGRect(origin: CGPoint(x: sideInset + 1.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + if self.item.isEdit { + iconFrame.origin.x -= 2.0 + } + transition.updateFrameAdditive(node: self.iconNode, frame: iconFrame) } let avatarsContent: AnimatedAvatarSetContext.Content let placeholderAvatarsContent: AnimatedAvatarSetContext.Content var avatarsPeers: [EnginePeer] = [] - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser || self.item.isEdit { } else if let recentPeers = self.item.message.reactionsAttribute?.recentPeers, !recentPeers.isEmpty { for recentPeer in recentPeers { if let peer = self.item.message.peers[recentPeer.peerId] { @@ -3504,12 +3559,15 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus guard let controller = self.getController() else { return } - self.item.action(controller, { [weak self] result in + self.item.action?(controller, { [weak self] result in self?.actionSelected(result) }, self.currentStats, self.customEmojiPacks, self.firstCustomEmojiReaction) } var isActionEnabled: Bool { + if self.item.action == nil { + return false + } var reactionCount = 0 for reaction in mergedMessageReactionsAndPeers(accountPeerId: self.item.context.account.peerId, accountPeer: nil, message: self.item.message).reactions { reactionCount += Int(reaction.count) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 63fb1e502f7..62b079614b5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -118,10 +118,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee case .quickReply: break default: - signal = .single({ _ in return .quickReplies([]) }) + signal = .single({ _ in return .quickReplies([], query) }) } } else { - signal = .single({ _ in return .quickReplies([]) }) + signal = .single({ _ in return .quickReplies([], query) }) } let querySignal = Signal.single(query) |> deliverOn(Queue(name: "nicegram.quick-replies")) @@ -129,7 +129,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = querySignal |> map { query -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let result: [String] = getQuickReplies(query: query, context: context) - return { _ in return .quickReplies(result) } + return { _ in return .quickReplies(result, query) } } |> castError(ChatContextQueryError.self) @@ -142,10 +142,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee case .hashtag: break default: - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } } else { - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags() @@ -157,7 +157,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee result.append(hashtag) } } - return { _ in return .hashtags(result) } + return { _ in return .hashtags(result, query) } } |> castError(ChatContextQueryError.self) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 76b38d024e4..7b4cd95b764 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -146,7 +146,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState var displayInputTextPanel = false if let peer = chatPresentationInterfaceState.renderedPeer?.peer { - if peer.id.isReplies { + if peer.id.isRepliesOrVerificationCodes { if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { return (currentPanel, nil) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 32d85e591d5..8e194084e2d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -171,7 +171,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } } if case let .peer(peerId) = presentationInterfaceState.chatLocation { - if peerId.isReplies { + if peerId.isRepliesOrVerificationCodes { if hasMessages { if case .search = currentButton?.action { return currentButton diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index 2a91e8555c7..cfcdc9843e7 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -326,6 +326,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { urlContext: .generic, navigationController: chatController.navigationController as? NavigationController, forceExternal: false, + forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let self, let chatController = interfaceInteraction.chatController() else { return diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 9163d271b52..d69089f0a3a 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -498,12 +498,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { var translateToLanguage: String? if let translationState = interfaceState.translationState, translationState.isEnabled { - translateToLanguage = translationState.toLang - if translateToLanguage == "nb" { - translateToLanguage = "no" - } else if translateToLanguage == "pt-br" { - translateToLanguage = "pt" - } + translateToLanguage = normalizeTranslationLanguage(translationState.toLang) } var currentTranslateToLanguageUpdated = false @@ -971,6 +966,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { controllerInteraction.openWebView(button.title, url, simple, .generic) case .requestPeer: break + case let .copyText(payload): + controllerInteraction.copyText(payload) } break diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index ebf77e9191a..21892f43af5 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -282,7 +282,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 8a2fbb07578..aa3a571d832 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -19,34 +19,6 @@ import AnimatedTextComponent private let labelFont = Font.regular(15.0) -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} - final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private struct Params: Equatable { var width: CGFloat @@ -100,6 +72,14 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private var totalMessageCount: Int? private var totalMessageCountDisposable: Disposable? + public var externalSearchResultsCount: Int32? { + didSet { + if let params = self.currentLayout?.params { + let _ = self.update(params: params, transition: .spring(duration: 0.4)) + } + } + } + override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { } @@ -223,7 +203,15 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var canChangeListMode = false var resultsTextString: [AnimatedTextComponent.Item] = [] - if let results = params.interfaceState.search?.resultsState { + if let externalSearchResultsCount = self.externalSearchResultsCount { + let value = presentationStringsFormattedNumber(externalSearchResultsCount, params.interfaceState.dateTimeFormat.groupingSeparator) + let suffix = params.interfaceState.strings.Chat_BottomSearchPanel_StoryCount(externalSearchResultsCount) + resultsTextString = [AnimatedTextComponent.Item( + id: "stories", + isUnbreakable: true, + content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) + )] + } else if let results = params.interfaceState.search?.resultsState { let displayTotalCount = results.completed ? results.messageIndices.count : Int(results.totalCount) if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { canChangeListMode = true @@ -237,7 +225,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) )] } else if params.interfaceState.displayHistoryFilterAsList { - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -247,7 +235,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else { let adjustedIndex = results.messageIndices.count - 1 - index - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( ".", "." ), id: "position", mapping: [ @@ -263,7 +251,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else if let count = self.tagMessageCount?.count ?? self.totalMessageCount { canChangeListMode = count != 0 - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -282,7 +270,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } var modeButtonTitle: [AnimatedTextComponent.Item] = [] - modeButtonTitle = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ + modeButtonTitle = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ 0: params.interfaceState.displayHistoryFilterAsList ? .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeChat) : .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeList) ]) @@ -346,7 +334,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var nextLeftX: CGFloat = 16.0 - if !self.alwaysShowTotalMessagesCount { + if !self.alwaysShowTotalMessagesCount && self.externalSearchResultsCount == nil { nextLeftX = 12.0 let calendarButtonSize = self.calendarButton.update( transition: .immediate, @@ -372,12 +360,28 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if let calendarButtonView = self.calendarButton.view { if calendarButtonView.superview == nil { self.view.addSubview(calendarButtonView) + + if !transition.animation.isImmediate { + calendarButtonView.alpha = 1.0 + transition.animateAlpha(view: calendarButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: calendarButtonView, from: 0.01, to: 1.0) + } } transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame) } nextLeftX += calendarButtonSize.width + 8.0 } else if let calendarButtonView = self.calendarButton.view { - calendarButtonView.removeFromSuperview() + if transition.animation.isImmediate { + calendarButtonView.removeFromSuperview() + } else { + transition.setAlpha(view: calendarButtonView, alpha: 0.0, completion: { finished in + if finished { + calendarButtonView.removeFromSuperview() + } + calendarButtonView.alpha = 1.0 + }) + transition.animateScale(view: calendarButtonView, from: 1.0, to: 0.01) + } } if displaySearchMembers { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 8c8d1bd54cd..22026231544 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1573,7 +1573,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isEditingMedia = !value.isEmpty isMediaEnabled = !value.isEmpty } else { - isMediaEnabled = false + isMediaEnabled = true } } diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index 32ca974cf6f..fb7481b9f0f 100644 --- a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift @@ -245,12 +245,7 @@ final class ChatTranslationPanelNode: ASDisplayNode { var addedLanguages = Set() var topLanguages: [String] = [] - var langCode = languageCode - if langCode == "nb" { - langCode = "no" - } else if langCode == "pt-br" { - langCode = "pt" - } + let langCode = normalizeTranslationLanguage(languageCode) var selectedLanguages: Set if let ignoredLanguages = settings.ignoredLanguages { diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index e5741bad8a7..719a632d4e5 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -156,7 +156,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index cabf9baa3e0..fdd0d2397d0 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -14,6 +14,7 @@ import AttachmentUI import SearchBarNode import ChatSendAudioMessageContextPreview import ChatSendMessageActionUI +import ContextUI class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable { private let context: AccountContext @@ -42,6 +43,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let multipleSelection: Bool private let requirePhoneNumbers: Bool + private let openProfile: ((EnginePeer) -> Void)? + private let sendMessage: ((EnginePeer) -> Void)? + private var _ready = Promise() override var ready: Promise { return self._ready @@ -105,6 +109,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.multipleSelection = params.multipleSelection self.requirePhoneNumbers = params.requirePhoneNumbers + self.openProfile = params.openProfile + self.sendMessage = params.sendMessage + self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -219,15 +226,15 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } self.contactsNode.requestOpenPeerFromSearch = { [weak self] peer in - self?.openPeer(peer: peer, action: .generic) + self?.openPeer(peer: peer, action: .generic, node: nil, gesture: nil) } self.contactsNode.contactListNode.activateSearch = { [weak self] in self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, _, _ in - self?.openPeer(peer: peer, action: action) + self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, node, gesture in + self?.openPeer(peer: peer, action: action, node: node, gesture: gesture) } self.contactsNode.contactListNode.suppressPermissionWarning = { [weak self] in @@ -357,7 +364,40 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } } - private func openPeer(peer: ContactListPeer, action: ContactListAction) { + private func openPeer(peer: ContactListPeer, action: ContactListAction, node: ASDisplayNode?, gesture: ContextGesture?) { + if case .more = action { + guard case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else { + return + } + + let presentationData = self.presentationData + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_SendMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.sendMessage?(EnginePeer(peer)) + } + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_OpenProfile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.openProfile?(EnginePeer(peer)) + } + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.present(contextController, in: .window(.root)) + return + } + self.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).startStrict(next: { [weak self] value in if let strongSelf = self { @@ -477,3 +517,17 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { func mainButtonAction() { } } + +private final class ContactContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 685113732ec..ba488f43c15 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -41,6 +41,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?, _ parameters: ChatSendMessageActionSheetController.SendParameters?) -> Void)? var dismiss: (() -> Void)? var cancelSearch: (() -> Void)? + var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var presentationData: PresentationData { didSet { diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index 523b598ef44..76e10d61070 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -15,39 +15,53 @@ import ChatControllerInteraction import ChatContextQuery import ChatInputContextPanelNode -private struct HashtagChatInputContextPanelEntryStableId: Hashable { - let text: String +private enum HashtagChatInputContextPanelEntryStableId: Hashable { + case generic + case peer + case hashtag(String) } private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let theme: PresentationTheme - let text: String + let peer: EnginePeer? + let title: String + let text: String? + let badge: String? + let hashtag: String + let revealed: Bool + let isAdditionalRecent: Bool // MARK: Nicegram QuickReplies let canDelete: Bool // - let revealed: Bool var stableId: HashtagChatInputContextPanelEntryStableId { - return HashtagChatInputContextPanelEntryStableId(text: self.text) + switch self.index { + case 0: + return .generic + case 1: + return .peer + default: + return .hashtag(self.title) + } } func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { // MARK: Nicegram QuickReplies, canDelete added - return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text, canDelete: self.canDelete, revealed: self.revealed) + return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, peer: peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent, canDelete: self.canDelete) } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed + return lhs.index == rhs.index && lhs.peer == rhs.peer && lhs.title == rhs.title && lhs.text == rhs.text && lhs.badge == rhs.badge && lhs.hashtag == rhs.hashtag && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed && lhs.isAdditionalRecent == rhs.isAdditionalRecent } static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { // MARK: Nicegram QuickReplies, canDelete added - return HashtagChatInputPanelItem(presentationData: ItemListPresentationData(presentationData), text: self.text, canDelete: self.canDelete, revealed: self.revealed, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) + return HashtagChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), peer: self.peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent, canDelete: self.canDelete, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) } } @@ -57,12 +71,12 @@ private struct HashtagChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { 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, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -72,6 +86,8 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private var currentEntries: [HashtagChatInputContextPanelEntry]? private var currentResults: [String] = [] + private var currentQuery: String = "" + private var currentPeer: EnginePeer? private var revealedHashtag: String? private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] @@ -97,15 +113,85 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } // MARK: Nicegram QuickReplies, canDelete added - func updateResults(_ results: [String], canDelete: Bool = true) { + func updateResults(_ results: [String], query: String, peer: EnginePeer?, canDelete: Bool = true) { self.currentResults = results + self.currentQuery = query + self.currentPeer = peer var entries: [HashtagChatInputContextPanelEntry] = [] var index = 0 var stableIds = Set() - for text in results { - // MARK: Nicegram QuickReplies, canDelete added - let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text, canDelete: canDelete, revealed: text == self.revealedHashtag) + + var isAdditionalRecent = false + if let peer, let _ = peer.addressName { + isAdditionalRecent = true + } + //TODO:localize + if query.count > 3 { + if let peer, let addressName = peer.addressName { + let genericEntry = HashtagChatInputContextPanelEntry( + index: 0, + theme: self.theme, + peer: nil, + title: "Use #\(query)", + text: "searches posts from all channels", + badge: nil, + hashtag: query, + revealed: false, + isAdditionalRecent: false, + // MARK: Nicegram QuickReplies + canDelete: canDelete + // + ) + stableIds.insert(genericEntry.stableId) + entries.append(genericEntry) + + var isGroup = false + if case let .channel(channel) = peer, case .group = channel.info { + isGroup = true + } + let peerEntry = HashtagChatInputContextPanelEntry( + index: 1, + theme: self.theme, + peer: peer, + title: "Use #\(query)@\(addressName)", + text: isGroup ? "searches only posts from this group" : "searches only posts from this channel", + badge: "NEW", + hashtag: "\(query)@\(addressName)", + revealed: false, + isAdditionalRecent: false, + // MARK: Nicegram QuickReplies + canDelete: canDelete + // + ) + stableIds.insert(peerEntry.stableId) + entries.append(peerEntry) + } + } + + index = 2 + + for hashtag in results { + if hashtag == query { + continue + } + if !hashtag.hasPrefix(query) { + continue + } + let entry = HashtagChatInputContextPanelEntry( + index: index, + theme: self.theme, + peer: hashtag.contains("@") ? peer : nil, + title: "#\(hashtag)", + text: nil, + badge: nil, + hashtag: hashtag, + revealed: hashtag == self.revealedHashtag, + isAdditionalRecent: isAdditionalRecent && !hashtag.contains("@"), + // MARK: Nicegram QuickReplies + canDelete: canDelete + // + ) if stableIds.contains(entry.stableId) { continue } @@ -119,10 +205,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { let firstTime = from == nil let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, presentationData: presentationData, setHashtagRevealed: { [weak self] text in + let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, setHashtagRevealed: { [weak self] text in if let strongSelf = self { strongSelf.revealedHashtag = text - strongSelf.updateResults(strongSelf.currentResults) + strongSelf.updateResults(strongSelf.currentResults, query: strongSelf.currentQuery, peer: strongSelf.currentPeer) } }, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { @@ -153,8 +239,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { if let range = hashtagQueryRange { let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) - let replacementText = text + " " - + let replacementText = text inputText.replaceCharacters(in: range, with: replacementText) let selectionPosition = range.lowerBound + (replacementText as NSString).length @@ -194,7 +279,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { //options.insert(.LowLatency) } else { options.insert(.AnimateTopItemPosition) - options.insert(.AnimateCrossfade) + if transition.insertions.isEmpty && transition.deletions.isEmpty && transition.updates.count <= 2 { + options.insert(.AnimateInsertion) + } else { + options.insert(.AnimateCrossfade) + } } var insets = UIEdgeInsets() diff --git a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift index d235e10db43..80223329e86 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift @@ -8,14 +8,22 @@ import Postbox import TelegramPresentationData import TelegramUIPreferences import ItemListUI +import AvatarNode +import AccountContext final class HashtagChatInputPanelItem: ListViewItem { + fileprivate let context: AccountContext fileprivate let presentationData: ItemListPresentationData - fileprivate let text: String + fileprivate let peer: EnginePeer? + fileprivate let title: String + fileprivate let text: String? + fileprivate let badge: String? + fileprivate let hashtag: String + fileprivate let revealed: Bool + fileprivate let isAdditionalRecent: Bool // MARK: Nicegram QuickReplies fileprivate let canDelete: Bool // - fileprivate let revealed: Bool fileprivate let setHashtagRevealed: (String?) -> Void private let hashtagSelected: (String) -> Void fileprivate let removeRequested: (String) -> Void @@ -23,13 +31,19 @@ final class HashtagChatInputPanelItem: ListViewItem { let selectable: Bool = true // MARK: Nicegram QuickReplies, canDelete added - public init(presentationData: ItemListPresentationData, text: String, canDelete: Bool, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, peer: EnginePeer?, title: String, text: String?, badge: String? = nil, hashtag: String, revealed: Bool, isAdditionalRecent: Bool, canDelete: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + self.context = context self.presentationData = presentationData + self.peer = peer + self.title = title self.text = text + self.badge = badge + self.hashtag = hashtag + self.revealed = revealed + self.isAdditionalRecent = isAdditionalRecent // MARK: Nicegram QuickReplies self.canDelete = canDelete // - self.revealed = revealed self.setHashtagRevealed = setHashtagRevealed self.hashtagSelected = hashtagSelected self.removeRequested = removeRequested @@ -86,14 +100,29 @@ final class HashtagChatInputPanelItem: ListViewItem { if self.revealed { self.setHashtagRevealed(nil) } else { - self.hashtagSelected(self.text) +// if self.isAdditionalRecent { +// self.hashtagSelected(self.hashtag) +// } else { + self.hashtagSelected(self.hashtag + " ") +// } } } } +private let avatarFont = avatarPlaceholderFont(size: 16.0) + final class HashtagChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 + + private let iconBackgroundLayer = SimpleLayer() + private let iconLayer = SimpleLayer() + private var avatarNode: AvatarNode? + + private let badgeBackgroundLayer = SimpleLayer() + + private let titleNode: TextNode private let textNode: TextNode + private let badgeNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -112,7 +141,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { private var validLayout: (CGSize, CGFloat, CGFloat)? init() { + self.iconBackgroundLayer.cornerRadius = 15.0 + self.badgeBackgroundLayer.cornerRadius = 4.0 + + self.titleNode = TextNode() self.textNode = TextNode() + self.badgeNode = TextNode() self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode.isLayerBacked = true @@ -127,9 +161,10 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.activateAreaNode.accessibilityTraits = [.button] super.init(layerBacked: false, dynamicBounce: false) - + self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) + self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.activateAreaNode) @@ -138,6 +173,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { override func didLoad() { super.didLoad() + self.view.layer.addSublayer(self.iconBackgroundLayer) + self.iconBackgroundLayer.addSublayer(self.iconLayer) + + self.view.layer.addSublayer(self.badgeBackgroundLayer) + self.addSubnode(self.badgeNode) + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) self.recognizer = recognizer recognizer.allowAnyDirection = false @@ -156,17 +197,25 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeBadgeLayout = TextNode.asyncLayout(self.badgeNode) + return { [weak self] item, params, mergedTop, mergedBottom in - let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let badgeFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 10.0 / 17.0)) - // MARK: Nicegram QuickReplies, baseWidth fix let leftInset: CGFloat = 15.0 + params.leftInset + let textLeftInset: CGFloat = 40.0 + // MARK: Nicegram QuickReplies, baseWidth fix let baseWidth = params.width - leftInset - params.rightInset - // MARK: Nicegram QuickReplies, # removed - let title = "\(item.text)" - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.badge ?? "", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth - badgeLayout.size.width, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text ?? "", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) @@ -174,26 +223,70 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) - + let revealOffset = strongSelf.revealOffset + if strongSelf.iconLayer.contents == nil { + strongSelf.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage + } + strongSelf.iconBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor + strongSelf.iconLayer.layerTintColor = item.presentationData.theme.list.itemCheckColors.foregroundColor.cgColor + strongSelf.badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + let _ = titleApply() let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + let _ = badgeApply() + + if textLayout.size.height > 0.0 { + let combinedHeight = titleLayout.size.height + textLayout.size.height + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0)), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0) + combinedHeight - textLayout.size.height), size: textLayout.size) + } else { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + } + + if badgeLayout.size.height > 0.0 { + let badgeFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 8.0, y: floorToScreenPixels(strongSelf.titleNode.frame.midY - badgeLayout.size.height / 2.0)), size: badgeLayout.size) + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0) + + strongSelf.badgeNode.frame = badgeFrame + strongSelf.badgeBackgroundLayer.frame = badgeBackgroundFrame + } strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom + let iconSize = CGSize(width: 30.0, height: 30.0) + strongSelf.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: iconSize) + strongSelf.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0)) + + if let peer = item.peer { + strongSelf.iconBackgroundLayer.isHidden = true + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarFont) + strongSelf.addSubnode(avatarNode) + strongSelf.avatarNode = avatarNode + } + avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer) + avatarNode.frame = strongSelf.iconBackgroundLayer.frame + } else { + strongSelf.iconBackgroundLayer.isHidden = false + } + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - textLeftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) - strongSelf.activateAreaNode.accessibilityLabel = title + strongSelf.activateAreaNode.accessibilityLabel = item.title strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size) // MARK: Nicegram QuickReplies, !item.canDelete ? [] : added @@ -206,7 +299,8 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { if let (_, leftInset, _) = self.validLayout { - transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + transition.updateFrameAdditive(layer: self.iconBackgroundLayer, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 12.0 + leftInset, y: self.iconBackgroundLayer.frame.minY), size: self.iconBackgroundLayer.frame.size)) + transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + 40.0 + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.frame.size)) } } @@ -285,6 +379,11 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let item = self.item { + if let _ = item.text { + return false + } + } return true } @@ -365,7 +464,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { guard let item = self.item else { return } - item.removeRequested(item.text) + item.removeRequested(item.hashtag) } private func setupAndAddRevealNode() { diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index d41b9923b6c..94770716c29 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -260,7 +260,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } imageDimensions = externalReference.content?.dimensions?.cgSize if externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let content = externalReference.content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)]) + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) imageResource = nil } diff --git a/submodules/TelegramUI/Sources/Nicegram/ChannelSubscriptionCheckerImpl.swift b/submodules/TelegramUI/Sources/Nicegram/ChannelSubscriptionCheckerImpl.swift deleted file mode 100644 index 43e575e313e..00000000000 --- a/submodules/TelegramUI/Sources/Nicegram/ChannelSubscriptionCheckerImpl.swift +++ /dev/null @@ -1,47 +0,0 @@ -import AccountContext -import FeatTasks -import SwiftSignalKit -import TelegramCore - -@available(iOS 13.0, *) -class ChannelSubscriptionCheckerImpl { - - // MARK: - Dependencies - - private let context: AccountContext - - // MARK: - Lifecycle - - init(context: AccountContext) { - self.context = context - } -} - -@available(iOS 13.0, *) -extension ChannelSubscriptionCheckerImpl: ChannelSubscriptionChecker { - public func isSubscribed(to id: ChannelId) async -> Bool { - await withCheckedContinuation { continuation in - _ = (context.engine.peers.resolvePeerByName(name: id) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> deliverOnMainQueue) - .start(next: { peer in - guard case let .channel(channel) = peer else { - continuation.resume(returning: false) - return - } - - switch channel.participationStatus { - case .member: - continuation.resume(returning: true) - case .left, .kicked: - continuation.resume(returning: false) - } - }) - } - } -} diff --git a/submodules/TelegramUI/Sources/Nicegram/FeedController.swift b/submodules/TelegramUI/Sources/Nicegram/FeedController.swift new file mode 100644 index 00000000000..d155fc19981 --- /dev/null +++ b/submodules/TelegramUI/Sources/Nicegram/FeedController.swift @@ -0,0 +1,81 @@ +import UIKit +import Display +import AccountContext +import SwiftSignalKit +import NGData +import NGStrings + +final class FeedController: ViewController { + private let context: AccountContext + + private var feedController: ChatController? + private var updateFeedDiposable: Disposable? + + init(context: AccountContext) { + self.context = context + super.init(navigationBarPresentationData: nil) + + let image = UIImage(bundleImageName: "feed")? + .sd_resizedImage(with: .init(width: 30, height: 30), scaleMode: .aspectFit)? + .withRenderingMode(.alwaysTemplate) + tabBarItem = UITabBarItem( + title: l("NicegramFeed.Title"), + image: image, + tag: 1 + ) + + _ = context.sharedContext.presentationData.start { [weak self] presentationData in + self?.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style + } + + self.updateFeedDiposable = (context.updateFeed |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { return } + + self.setupFeed(with: context) + }) + + + if NGSettings.feedPeer[context.account.id.int64] != nil { + setupFeed(with: context) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + updateFeedDiposable?.dispose() + updateFeedDiposable = nil + } + + override func containerLayoutUpdated( + _ layout: ContainerViewLayout, + transition: ContainedViewLayoutTransition + ) { + super.containerLayoutUpdated(layout, transition: transition) + + feedController?.view.frame = view.bounds + feedController?.containerLayoutUpdated(layout, transition: transition) + } + + private func setupFeed( + with context: AccountContext + ) { + guard let id = NGSettings.feedPeer[context.account.id.int64] else { return } + + feedController?.removeFromParent() + feedController?.view.removeFromSuperview() + + let feedController = ChatControllerImpl( + context: context, + chatLocation: .peer(id: id), + isFeed: true + ) + + addChild(feedController) + view.addSubview(feedController.view) + + self.feedController = feedController + } +} diff --git a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift index 7b92293466b..01ffe748dfc 100644 --- a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift +++ b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift @@ -1,4 +1,5 @@ import FeatAssistant +import FeatAttentionEconomy import Foundation import AccountContext import Display @@ -6,8 +7,6 @@ import FeatAvatarGeneratorUI import FeatImagesHubUI import FeatOnboarding import FeatPremiumUI -import FeatRewardsUI -import FeatTasks import NGAiChatUI import NGAnalytics import NGEntryPoint @@ -16,11 +15,13 @@ import NGCore import class NGCoreUI.SharedLoadingView import NGModels import NGRemoteConfig +import NGRepoUser import NGSpecialOffer import NGUI import TelegramPresentationData import UIKit +@MainActor class NGDeeplinkHandler { // MARK: - Dependencies @@ -60,12 +61,14 @@ class NGDeeplinkHandler { return handleAssistant(url: url) case "assistant-auth": return handleLoginToAssistant(url: url) + case "attention-economy": + if #available(iOS 15.0, *) { + AttPresenter().present() + } + return true case "avatarGenerator": if #available(iOS 15.0, *) { - Task { @MainActor in - guard let topController = UIApplication.topViewController else { - return - } + if let topController = UIApplication.topViewController { AvatarGeneratorUIHelper().navigateToGenerationFlow( from: topController ) @@ -74,9 +77,7 @@ class NGDeeplinkHandler { return true case "avatarMyGenerations": if #available(iOS 15.0, *) { - Task { @MainActor in - AvatarGeneratorUIHelper().navigateToGenerator() - } + AvatarGeneratorUIHelper().navigateToGenerator() } return true case "generateImage": @@ -85,39 +86,16 @@ class NGDeeplinkHandler { return handleNicegramPremium(url: url) case "onboarding": return handleOnboarding(url: url) - case "profit": - if #available(iOS 15.0, *) { - Task { @MainActor in - RewardsUITgHelper.showRewards() - } - } - return true case "specialOffer": - if #available(iOS 13.0, *) { - return handleSpecialOffer(url: url) - } else { - return false - } + return handleSpecialOffer(url: url) case "refferaldraw": if #available(iOS 15.0, *) { - Task { @MainActor in - AssistantTgHelper.showReferralDrawFromDeeplink() - } - return true - } else { - return false - } - case "task": - if #available(iOS 15.0, *) { - let taskDeeplinkHandler = TasksContainer.shared.taskDeeplinkHandler() - taskDeeplinkHandler.handle(url) + AssistantTgHelper.showReferralDrawFromDeeplink() } return true case "tgAuthSuccess": if #available(iOS 15.0, *) { - Task { @MainActor in - TgAuthSuccessPresenter().presentIfNeeded() - } + TgAuthSuccessPresenter().presentIfNeeded() } return true default: @@ -131,37 +109,23 @@ class NGDeeplinkHandler { private extension NGDeeplinkHandler { func handleAiAuth(url: URL) -> Bool { - if #available(iOS 13.0, *) { - Task { @MainActor in - AiChatUITgHelper.routeToAiOnboarding() - } - return true - } - return false + AiChatUITgHelper.routeToAiOnboarding() + return true } func handleAi(url: URL) -> Bool { - if #available(iOS 13.0, *) { - Task { @MainActor in - AiChatUITgHelper.tryRouteToAiChatBotFromDeeplink() - } - return true - } - return false + AiChatUITgHelper.tryRouteToAiChatBotFromDeeplink() + return true } func handleGenerateImage(url: URL) -> Bool { if #available(iOS 15.0, *) { - Task { @MainActor in - ImagesHubUITgHelper.showFeed( - source: .deeplink, - forceGeneration: true - ) - } - return true - } else { - return false + ImagesHubUITgHelper.showFeed( + source: .deeplink, + forceGeneration: true + ) } + return true } func handleNicegramPremium(url: URL) -> Bool { @@ -173,15 +137,11 @@ private extension NGDeeplinkHandler { func handleAssistant(url: URL) -> Bool { if #available(iOS 15.0, *) { - Task { @MainActor in - AssistantTgHelper.routeToAssistant( - source: .deeplink - ) - } - return true - } else { - return false + AssistantTgHelper.routeToAssistant( + source: .deeplink + ) } + return true } func handleOnboarding(url: URL) -> Bool { @@ -211,20 +171,35 @@ private extension NGDeeplinkHandler { } Task { @MainActor in - LoginViewPresenter().present( - feature: LoginFeature() - ) + let getCurrentUserUseCase = RepoUserContainer.shared.getCurrentUserUseCase() + let initTgLoginUseCase = AuthContainer.shared.initTgLoginUseCase() + let toastManager = CoreContainer.shared.toastManager() + let urlOpener = CoreContainer.shared.urlOpener() + + guard !getCurrentUserUseCase.isAuthorized() else { + return + } + + SharedLoadingView.start() + + let result = await initTgLoginUseCase(source: .general) + + SharedLoadingView.stop() + switch result { + case let .success(url): + urlOpener.open(url) + case let .failure(error): + toastManager.showError(error) + } } + return true } - @available(iOS 13.0, *) func handleSpecialOffer(url: URL) -> Bool { - Task { @MainActor in - SpecialOfferTgHelper.showSpecialOfferFromDeeplink( - id: url.queryItems["id"] - ) - } + SpecialOfferTgHelper.showSpecialOfferFromDeeplink( + id: url.queryItems["id"] + ) return true } } diff --git a/submodules/TelegramUI/Sources/Nicegram/NicegramMapChatHistoryEntries.swift b/submodules/TelegramUI/Sources/Nicegram/NicegramMapChatHistoryEntries.swift new file mode 100644 index 00000000000..abcd9858c20 --- /dev/null +++ b/submodules/TelegramUI/Sources/Nicegram/NicegramMapChatHistoryEntries.swift @@ -0,0 +1,219 @@ +import FeatAttentionEconomy +import Foundation +import NGUtils + +import ChatHistoryEntry +import Display +import Postbox +import TelegramPresentationData + +private let TOP_OFFSET_FROM_VISIBLE_RANGE = 10 +private let BOTTOM_OFFSET_FROM_VISIBLE_RANGE = 10 +private let THRESHOLD_MEMBERS = 1000 + +struct NicegramAdInChat { + let ad: AttAd? + let forceRemove: Bool +} + +func nicegramMapChatHistoryEntries( + oldEntries: [ChatHistoryEntry], + newEntries: [ChatHistoryEntry], + nicegramAd: NicegramAdInChat, + visibleItemRange: ListViewVisibleItemRange?, + chatPresentationData: ChatPresentationData, + cachedPeerData: CachedPeerData? +) -> [ChatHistoryEntry] { + var result = newEntries + + guard !nicegramAd.forceRemove else { + return result + } + let nicegramAd = nicegramAd.ad + + guard areAdsAllowed( + cachedPeerData: cachedPeerData + ) else { + return result + } + + guard let visibleRange = parseVisibleRange( + oldEntries: oldEntries, + visibleItemRange: visibleItemRange + ) else { + return result + } + + result = insertOldAdEntries( + oldEntries: oldEntries, + result: result, + nicegramAd: nicegramAd, + visibleRange: visibleRange + ) + result = updateOldAdEntries( + result: result, + nicegramAd: nicegramAd + ) + result = insertNewAdEntries( + result: result, + nicegramAd: nicegramAd, + visibleRange: visibleRange, + chatPresentationData: chatPresentationData + ) + + return result +} + +private func areAdsAllowed( + cachedPeerData: CachedPeerData? +) -> Bool { + guard #available(iOS 15.0, *) else { + return false + } + + let membersCount = getMembersCount(cachedPeerData: cachedPeerData) + if let membersCount, membersCount > THRESHOLD_MEMBERS { + return true + } else { + return false + } +} + +private func parseVisibleRange( + oldEntries: [ChatHistoryEntry], + visibleItemRange: ListViewVisibleItemRange? +) -> Range? { + guard let visibleItemRange else { + return nil + } + let leftIndex = oldEntries.count - 1 - visibleItemRange.lastIndex + let rightIndex = oldEntries.count - 1 - visibleItemRange.firstIndex + guard leftIndex <= rightIndex else { + return nil + } + return leftIndex.. +) -> [ChatHistoryEntry] { + struct OldEntry { + let entry: ChatHistoryEntry + let index: Int + } + + var result = result + + let filteredOldEntries = oldEntries.enumerated() + .filter { _, entry in + if case .NicegramAdEntry = entry { + true + } else { + result.contains { entry.stableId == $0.stableId } + } + } + .map { OldEntry(entry: $1, index: $0) } + + filteredOldEntries.forEachWithNeighbors { left, mid, right in + let entry = mid.entry + let index = mid.index + + guard case let .NicegramAdEntry(_, ad, _) = entry else { + return + } + + let isCurrentAd = (nicegramAd?.adId == ad.adId) + let isVisible = visibleRange.contains(index) + + if isCurrentAd || isVisible { + let targetIndex: Int + if let left, let index = result.firstIndex(where: { $0.stableId == left.entry.stableId }) { + targetIndex = index + 1 + } else if let right, let index = result.firstIndex(where: { $0.stableId == right.entry.stableId }) { + targetIndex = index + } else { + targetIndex = 0 + } + + result.insert(entry, at: targetIndex) + } + } + + return result +} + +private func updateOldAdEntries( + result: [ChatHistoryEntry], + nicegramAd: AttAd? +) -> [ChatHistoryEntry] { + guard let nicegramAd else { + return result + } + + return result.map { entry in + if case let .NicegramAdEntry(id, ad, presentationData) = entry, + ad.adId == nicegramAd.adId { + .NicegramAdEntry(id, nicegramAd, presentationData) + } else { + entry + } + } +} + +private func insertNewAdEntries( + result: [ChatHistoryEntry], + nicegramAd: AttAd?, + visibleRange: Range, + chatPresentationData: ChatPresentationData +) -> [ChatHistoryEntry] { + var result = result + + guard let nicegramAd else { + return result + } + + let alreadyContainsAd = result.contains { + if case let .NicegramAdEntry(_, ad, _) = $0, + ad.adId == nicegramAd.adId { + true + } else { + false + } + } + if !alreadyContainsAd { + let indicesToInsert = [ + visibleRange.lowerBound - TOP_OFFSET_FROM_VISIBLE_RANGE, + visibleRange.upperBound + BOTTOM_OFFSET_FROM_VISIBLE_RANGE + ] + indicesToInsert.forEach { index in + let index = index.clamped(to: result.startIndex...result.endIndex) + + if index != result.startIndex, + index != result.endIndex { + result.insert( + .NicegramAdEntry( + UUID().uuidString, + nicegramAd, + chatPresentationData + ), + at: index + ) + } + } + } + + return result +} + +private extension Array { + func forEachWithNeighbors(_ body: (Element?, Element, Element?) -> Void) { + for i in 0.. 0 ? self[i - 1] : nil + let right = i < self.count - 1 ? self[i + 1] : nil + body(left, self[i], right) + } + } +} diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a7712b527bb..889f521c1d3 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -250,9 +250,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { let subject: BrowserScreen.Subject if file.mimeType == "application/pdf" { - subject = .pdfDocument(file: file, canShare: canShare) + subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } else { - subject = .document(file: file, canShare: canShare) + subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } let controller = BrowserScreen(context: params.context, subject: subject) controller.openDocument = { file, canShare in diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 980473852cb..acb60a63996 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -58,6 +58,7 @@ func openResolvedUrlImpl( urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, + forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, @@ -222,7 +223,16 @@ func openResolvedUrlImpl( case let .stickerPack(name, _): dismissInput() - let controller = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: .name(name), stickerPacks: [.name(name)], parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, actionPerformed: { actions in + let controller = StickerPackScreen( + context: context, + updatedPresentationData: updatedPresentationData, + mainStickerPack: .name(name), + stickerPacks: [.name(name)], + ignoreCache: forceUpdate, + parentNavigationController: navigationController, + sendSticker: sendSticker, + sendEmoji: sendEmoji, + actionPerformed: { actions in if actions.count > 1, let first = actions.first { if case .add = first.2 { present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e9e492ee343..7ce1dfb735c 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -170,6 +170,8 @@ private func extractNicegramDeeplink(from link: String) -> String? { func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, skipNicegramProcessing: Bool = false, dismissInput: @escaping () -> Void) { // MARK: Nicegram if !skipNicegramProcessing { + let url = NGCore.UrlUtils.normalizeNicegramDeeplink(url) + if let nicegramDeeplink = extractNicegramDeeplink(from: url) { openExternalUrlImpl(context: context, urlContext: urlContext, url: nicegramDeeplink, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) return @@ -253,7 +255,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if case let .externalUrl(value) = resolved { context.sharedContext.applicationBindings.openUrl(value) } else { - context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in switch navigation { case .info: if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { @@ -1079,9 +1081,6 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let convertedUrl = convertedUrl { handleInternalUrl(convertedUrl) - // MARK: Nicegram Deeplink, added 'else' block - } else { - showUpdateAppAlert() } return } @@ -1189,33 +1188,3 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur continueHandling() } } - -// MARK: Nicegram Deeplink -private func showUpdateAppAlert() { - let alert = UIAlertController( - title: "Update the app", - message: "Please update the app to use the newest features!", - preferredStyle: .alert - ) - - alert.addAction( - UIAlertAction( - title: "Close", - style: .cancel - ) - ) - - alert.addAction( - UIAlertAction( - title: "Update", - style: .default, - handler: { _ in - let urlOpener = CoreContainer.shared.urlOpener() - urlOpener.open(.appStoreAppUrl) - } - ) - ) - - UIApplication.topViewController?.present(alert, animated: true) -} -// diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 8d9f42b0480..f51c17d3597 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -165,7 +165,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -185,6 +187,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift index 92ec3281105..75f774190b5 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift @@ -24,7 +24,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private var contentNodeSnapshot: UIView? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(tapped: @escaping () -> Void) { self.tapped = tapped @@ -58,9 +58,9 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { self.progressNode.status = contentNode.status if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -74,15 +74,15 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { if let snapshot = snapshot { self.contentContainerNode.view.addSubview(snapshot) - if let _ = self.validLayoutSize { + if let _ = self.validLayout { snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size) } } } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) self.contentContainerNode.cornerRadius = size.width / 2.0 @@ -96,7 +96,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -0.5, dy: -0.5)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } if let contentNodeSnapshot = self.contentNodeSnapshot { diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift index a7c1d30bfa8..6e801b26675 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift @@ -40,14 +40,14 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode { var playbackEnded: (() -> Void)? - init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { + init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { self.close = close self.content = content var togglePlayPauseImpl: (() -> Void)? let decoration = OverlayInstantVideoDecoration(tapped: { togglePlayPauseImpl?() }) - self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) + self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) self.decoration = decoration super.init() diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 9b28b54b499..9eb5ba1858e 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -72,7 +72,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } else { return SharedMediaPlaybackData(type: .music, source: source) } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackData(type: .instantVideo, source: source) } else { @@ -129,7 +129,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { displayData = SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption) } return displayData - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor.flatMap(EnginePeer.init), peer: self.message.peers[self.message.id.peerId].flatMap(EnginePeer.init), timestamp: self.message.timestamp) } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a7436381e62..226fe012762 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -71,6 +71,10 @@ import StarsTransferScreen import StarsTransactionScreen import StarsWithdrawalScreen import MiniAppListScreen +import GiftOptionsScreen +import GiftViewScreen +import StarsIntroScreen +import ContentReportScreen import NGCore import NGData @@ -1741,8 +1745,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } - public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { - openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) + public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { + openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, forceUpdate: forceUpdate, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) } public func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController { @@ -1907,7 +1911,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -1927,6 +1933,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in }, forceUpdateWarpContents: { + }, playShakeAnimation: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) @@ -2050,8 +2057,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return inputPanelNode } - public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController { - return HashtagSearchController(context: context, peer: peer, query: query, all: all) + public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController { + return HashtagSearchController(context: context, peer: peer, query: query, mode: stories ? .chatOnly : .generic, stories: stories, forceDark: forceDark) } public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { @@ -2330,29 +2337,92 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } + public func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var presentBirthdayPickerImpl: (() -> Void)? + let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false) + + let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) + |> map { birthday in + if birthday == nil { + return [ContactListAdditionalOption( + title: presentationData.strings.Premium_Gift_ContactSelection_AddBirthday, + icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), + action: { + presentBirthdayPickerImpl?() + }, + clearHighlightAutomatically: true + )] + } else { + return [] + } + } + |> deliverOnMainQueue + + let options = Promise<[StarsGiftOption]>() + options.set(context.engine.payments.starsGiftOptions(peerId: nil)) + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: starsMode, + autoDismiss: false, + title: { strings in return strings.Stars_Purchase_GiftStars }, + options: contactOptions + )) + let _ = (controller.result + |> deliverOnMainQueue).start(next: { result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + completion([peer.id]) + } + }) + + presentBirthdayPickerImpl = { [weak controller] in + guard let controller else { + return + } + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .setupBirthday).startStandalone() + + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { + settingsPromise = current + } else { + settingsPromise = Promise() + settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + } + let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { + context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in + controller?.push(c) + }) + }, completion: { [weak controller] value in + let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + }), in: .current) + }) + controller.push(birthdayController) + } + + return controller + } + public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let limit: Int32 = 10 - var reachedLimitImpl: ((Int32) -> Void)? var presentBirthdayPickerImpl: (() -> Void)? - let mode: ContactMultiselectionControllerMode - var starsMode: ContactSelectionControllerMode = .generic + var mode: ContactSelectionControllerMode = .generic var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? + if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true) + mode = .starsGifting(birthdays: birthdays, hasActions: true) currentBirthdays = birthdays } else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: true) - currentBirthdays = birthdays - } else if case let .stars(birthdays) = source { - mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false) - starsMode = .starsGifting(birthdays: birthdays, hasActions: false) + mode = .starsGifting(birthdays: birthdays, hasActions: true) currentBirthdays = birthdays } else { - mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true) + mode = .starsGifting(birthdays: nil, hasActions: true) } - + let contactOptions: Signal<[ContactListAdditionalOption], NoError> if currentBirthdays != nil || "".isEmpty { contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) @@ -2378,105 +2448,31 @@ public final class SharedAccountContextImpl: SharedAccountContext { var openProfileImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)? - let controller: ViewController - if case .stars = source { - let options = Promise<[StarsGiftOption]>() - options.set(context.engine.payments.starsGiftOptions(peerId: nil)) - let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( - context: context, - mode: starsMode, - autoDismiss: false, - title: { strings in return strings.Stars_Purchase_GiftStars }, - options: contactOptions - )) - let _ = (contactsController.result - |> deliverOnMainQueue).start(next: { result in - if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { - completion?([peer.id]) - } - }) - controller = contactsController - } else { - let options = Promise<[PremiumGiftCodeOption]>() - options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) - let contactsController = context.sharedContext.makeContactMultiselectionController( - ContactMultiselectionControllerParams( - context: context, - mode: mode, - options: contactOptions, - isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { - return true - } else { - return false - } - }, - limit: limit, - reachedLimit: { limit in - reachedLimitImpl?(limit) - }, - openProfile: { peer in - openProfileImpl?(peer) - }, - sendMessage: { peer in - sendMessageImpl?(peer) - } - ) - ) - let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) - .startStandalone(next: { [weak contactsController] result, options in - guard let controller = contactsController else { - return - } - var peerIds: [PeerId] = [] - if case let .result(peerIdsValue, _) = result { - peerIds = peerIdsValue.compactMap({ peerId in - if case let .peer(peerId) = peerId { - return peerId - } else { - return nil - } - }) - } - guard !peerIds.isEmpty else { - return - } - - let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - var pushImpl: ((ViewController) -> Void)? - var filterImpl: (() -> Void)? - let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in - pushImpl?(c) - }, completion: { - filterImpl?() - - if case .chatList = source, let _ = currentBirthdays { - let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() - } - }) - pushImpl = { [weak giftController] c in - giftController?.push(c) - } - filterImpl = { [weak giftController] in - if let navigationController = giftController?.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } - navigationController.setViewControllers(controllers, animated: true) - } - } - controller.push(giftController) - }) - controller = contactsController - } - - reachedLimitImpl = { [weak controller] limit in - guard let controller else { - return + let options = Promise<[PremiumGiftCodeOption]>() + options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: mode, + autoDismiss: false, + title: { strings in return presentationData.strings.Gift_PremiumOrStars_Title }, + options: contactOptions, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) } - HapticFeedback().error() - controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - + )) + let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get()) + .startStandalone(next: { [weak controller] result, options in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { + let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions) + giftController.navigationPresentation = .modal + controller?.push(giftController) + } + }) + sendMessageImpl = { [weak self, weak controller] peer in guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { return @@ -2499,7 +2495,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, - avatarInitiallyExpanded: true, + avatarInitiallyExpanded: peer.smallProfileImage != nil, fromChat: false, requestsContext: nil ) { @@ -2537,6 +2533,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } + public func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) -> ViewController { + guard let starsContext = context.starsContext else { + fatalError() + } + let controller = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peerId, premiumOptions: premiumOptions) + controller.navigationPresentation = .modal + return controller + } + public func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController { let mappedSubject: PremiumPrivacyScreen.Subject let introSource: PremiumIntroSource @@ -2926,6 +2931,23 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionScreen(context: context, subject: .boost(peerId, boost)) } + public func makeStarsIntroScreen(context: AccountContext) -> ViewController { + return StarsIntroScreen(context: context) + } + + public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController { + return GiftViewScreen(context: context, subject: .message(message)) + } + + public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) { + let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil) + |> deliverOnMainQueue).startStandalone(next: { result in + if case let .options(title, options) = result { + present(ContentReportScreen(context: context, subject: subject, title: title, options: options, forceDark: forceDark, completed: completion, requestSelectMessages: requestSelectMessages)) + } + }) + } + public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { return MiniAppListScreen.initialData(context: context) } @@ -2934,8 +2956,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } - public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { - openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } } @@ -2961,6 +2983,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var hintGroupInCommon: PeerId? var forumTopicThread: ChatReplyThreadMessage? var isMyProfile = false + var switchToGifts = false switch mode { case let .nearbyPeer(distance): @@ -2977,10 +3000,13 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation forumTopicThread = thread case .myProfile: isMyProfile = true + case .myProfileGifts: + isMyProfile = true + switchToGifts = true default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, switchToGifts: switchToGifts) } else if peer is TelegramSecretChat { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index a5961e28768..6c6229f2017 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -236,7 +236,7 @@ final class SharedMediaPlayer { if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { case let .telegramFile(fileReference, _, _): - let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in + let videoNode = OverlayInstantVideoNode(accountId: strongSelf.account.id, postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) strongSelf.playbackItem = .instantVideo(videoNode) diff --git a/submodules/TelegramUI/Sources/SpotlightContacts.swift b/submodules/TelegramUI/Sources/SpotlightContacts.swift index 3437c5e1a4c..699ac90b876 100644 --- a/submodules/TelegramUI/Sources/SpotlightContacts.swift +++ b/submodules/TelegramUI/Sources/SpotlightContacts.swift @@ -186,12 +186,38 @@ private func manageableSpotlightContacts(appBasePath: String, accounts: Signal<[ return accounts |> mapToSignal { accounts -> Signal<[[EnginePeer.Id: SpotlightIndexStorageItem]], NoError> in return combineLatest(queue: queue, accounts.map { account -> Signal<[EnginePeer.Id: SpotlightIndexStorageItem], NoError> in - return TelegramEngine(account: account).data.subscribe( - TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) + let engine = TelegramEngine(account: account) + let recentApps = engine.peers.recentApps() + |> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in + return engine.data.get( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ) + ) + |> map { result -> [EnginePeer] in + var peers: [EnginePeer] = [] + for (_, maybePeer) in result { + if let peer = maybePeer { + peers.append(peer) + } + } + return peers + } + } + return combineLatest( + engine.data.subscribe( + TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) + ), + recentApps ) - |> map { view -> [EnginePeer.Id: SpotlightIndexStorageItem] in + |> map { view, recentApps -> [EnginePeer.Id: SpotlightIndexStorageItem] in var result: [EnginePeer.Id: SpotlightIndexStorageItem] = [:] - for peer in view.peers { + var peers: [EnginePeer] = [] + peers.append(contentsOf: view.peers) + peers.append(contentsOf: recentApps) + for peer in peers { if case let .user(user) = peer { let avatarSourcePath = smallestImageRepresentation(user.photo).flatMap { representation -> String? in let resourcePath = account.postbox.mediaBox.resourcePath(representation.resource) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 2b2520a909f..292adb6252d 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -11,6 +11,7 @@ import NGGrumUI // // MARK: Nicegram imports import NGData +import NGStrings // import Foundation import UIKit @@ -89,7 +90,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon // MARK: Nicegram Assistant public var assistantController: ViewController? // - +// MARK: Nicegram NCG-6373 Feed tab + private var feedController: FeedController? +// private let context: AccountContext public var rootTabController: TabBarController? @@ -260,8 +263,20 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if showCallsTab { controllers.append(callListController) } + controllers.append(chatListController) +// MARK: Nicegram NCG-6373 Feed tab + let feedController = FeedController( + context: self.context + ) + if NGSettings.showFeedTab && + NGSettings.feedPeer[context.account.id.int64] != nil { + controllers.append(feedController) + } + self.feedController = feedController +// + // MARK: Nicegram Assistant if #available(iOS 15.0, *) { let assistantController = NativeControllerWrapper( @@ -342,10 +357,16 @@ public final class TelegramRootController: NavigationController, TelegramRootCon // MARK: Nicegram Assistant // calculate chatListControllerIndex (instead of (controllers.count - 2)) - let chatListControllerIndex = controllers.firstIndex { + var selectedControllerIndex = controllers.firstIndex { $0 is ChatListController } ?? 0 - tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : chatListControllerIndex) + if NGSettings.showFeedTab && + NGSettings.feedPeer[context.account.id.int64] != nil { + selectedControllerIndex = controllers.firstIndex { + $0 is FeedController + } ?? 0 + } + tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : selectedControllerIndex) self.contactsController = contactsController self.callListController = callListController @@ -369,7 +390,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon controllers.append(self.callListController!) } controllers.append(self.chatListController!) - +// MARK: Nicegram NCG-6373 Feed tab + if NGSettings.showFeedTab && + NGSettings.feedPeer[context.account.id.int64] != nil { + controllers.append(self.feedController!) + } +// // MARK: Nicegram Assistant if let assistantController, NGSettings.showNicegramTab { diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 5aae48a9d03..ee253628e04 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -26,7 +26,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n guard let peer = peer else { return } - context.sharedContext.openResolvedUrl(.peer(peer._asPeer(), navigation), context: context, urlContext: .generic, navigationController: (controller?.navigationController as? NavigationController), forceExternal: false, openPeer: { (peer, navigation) in + context.sharedContext.openResolvedUrl(.peer(peer._asPeer(), navigation), context: context, urlContext: .generic, navigationController: (controller?.navigationController as? NavigationController), forceExternal: false, forceUpdate: false, openPeer: { (peer, navigation) in switch navigation { case let .chat(_, subject, peekData): if let navigationController = controller?.navigationController as? NavigationController { @@ -102,7 +102,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n (controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true) case .boost, .chatFolder, .join: if let navigationController = controller.navigationController as? NavigationController { - openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in + openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigateToPeer in openResolvedPeerImpl(peer, navigateToPeer) }, sendFile: nil, sendSticker: nil, sendEmoji: nil, joinVoiceChat: nil, present: { c, a in controller.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 104c2ef729a..29205d72969 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -59,6 +59,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var allowWebViewInspection: Bool public var disableReloginTokens: Bool public var liveStreamV2: Bool + public var dynamicStreaming: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -96,7 +97,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCallMute: false, allowWebViewInspection: false, disableReloginTokens: false, - liveStreamV2: false + liveStreamV2: false, + dynamicStreaming: false ) } @@ -134,7 +136,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCallMute: Bool, allowWebViewInspection: Bool, disableReloginTokens: Bool, - liveStreamV2: Bool + liveStreamV2: Bool, + dynamicStreaming: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -170,6 +173,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allowWebViewInspection = allowWebViewInspection self.disableReloginTokens = disableReloginTokens self.liveStreamV2 = liveStreamV2 + self.dynamicStreaming = dynamicStreaming } public init(from decoder: Decoder) throws { @@ -209,6 +213,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false + self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming") ?? false } public func encode(to encoder: Encoder) throws { @@ -248,6 +253,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens") try container.encode(self.liveStreamV2, forKey: "liveStreamV2") + try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming") } } diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index 5e2beb637c2..fc8fceb5046 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -564,7 +564,10 @@ public func isAutodownloadEnabledForAnyPeerType(category: MediaAutoDownloadCateg return category.contacts || category.otherPrivate || category.groups || category.channels } -public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?, isStory: Bool = false) -> Bool { +public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?, isStory: Bool = false, isAd: Bool = false) -> Bool { + if isAd { + return true + } if (networkType == .cellular && !settings.cellular.enabled) || (networkType == .wifi && !settings.wifi.enabled) { return false } diff --git a/submodules/TelegramUniversalVideoContent/BUILD b/submodules/TelegramUniversalVideoContent/BUILD index eaa02aa5591..d48180b6d80 100644 --- a/submodules/TelegramUniversalVideoContent/BUILD +++ b/submodules/TelegramUniversalVideoContent/BUILD @@ -1,4 +1,44 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "HlsBundleContents", + srcs = glob([ + "HlsBundle/**", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "HlsBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.TelegramUniversalVideoContent + CFBundleDevelopmentRegion + en + CFBundleName + TelegramUniversalVideoContent + """ +) + +apple_resource_bundle( + name = "HlsBundle", + infoplists = [ + ":HlsBundleInfoPlist", + ], + resources = [ + ":HlsBundleContents", + ], +) swift_library( name = "TelegramUniversalVideoContent", @@ -9,6 +49,9 @@ swift_library( copts = [ #"-warnings-as-errors", ], + data = [ + ":HlsBundle", + ], deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", @@ -23,6 +66,8 @@ swift_library( "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/AppBundle:AppBundle", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/TelegramVoip", + "//submodules/ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js b/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js new file mode 100644 index 00000000000..961d4bc6969 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js @@ -0,0 +1 @@ +(()=>{"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,s,i,r,n,a={exports:{}};e=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,s=/^(?=([^\/?#]*))\1([^]*)$/,i=/(?:\/|^)\.(?=\/)/g,r=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,a.exports=n={buildAbsoluteURL:function(t,e,i){if(i=i||{},t=t.trim(),!(e=e.trim())){if(!i.alwaysNormalize)return t;var r=n.parseURL(t);if(!r)throw new Error("Error trying to parse base URL.");return r.path=n.normalizePath(r.path),n.buildURLFromParts(r)}var a=n.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return i.alwaysNormalize?(a.path=n.normalizePath(a.path),n.buildURLFromParts(a)):e;var o=n.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=s.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var h={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(h.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var d=o.path,c=d.substring(0,d.lastIndexOf("/")+1)+a.path;h.path=n.normalizePath(c)}else h.path=o.path,a.params||(h.params=o.params,a.query||(h.query=o.query));return null===h.path&&(h.path=i.alwaysNormalize?n.normalizePath(a.path):a.path),n.buildURLFromParts(h)},parseURL:function(t){var s=e.exec(t);return s?{scheme:s[1]||"",netLoc:s[2]||"",path:s[3]||"",params:s[4]||"",query:s[5]||"",fragment:s[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(i,"");t.length!==(t=t.replace(r,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}};var o=a.exports;function l(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),s.push.apply(s,i)}return s}function h(t){for(var e=1;e`):E}(e)}))}const A=S,R=/^(\d+)x(\d+)$/,b=/(.+?)=(".*?"|.*?)(?:,|$)/g;class k{constructor(t){"string"==typeof t&&(t=k.parseAttrList(t)),u(this,t)}get clientAttrs(){return Object.keys(this).filter((t=>"X-"===t.substring(0,2)))}decimalInteger(t){const e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e}hexadecimalInteger(t){if(this[t]){let e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;const s=new Uint8Array(e.length/2);for(let t=0;tNumber.MAX_SAFE_INTEGER?1/0:e}decimalFloatingPoint(t){return parseFloat(this[t])}optionalFloat(t,e){const s=this[t];return s?parseFloat(s):e}enumeratedString(t){return this[t]}bool(t){return"YES"===this[t]}decimalResolution(t){const e=R.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}}static parseAttrList(t){let e;const s={};for(b.lastIndex=0;null!==(e=b.exec(t));){let t=e[2];0===t.indexOf('"')&&t.lastIndexOf('"')===t.length-1&&(t=t.slice(1,-1));s[e[1].trim()]=t}return s}}function w(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}class D{constructor(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){const s=e.attr;for(const e in s)if(Object.prototype.hasOwnProperty.call(t,e)&&t[e]!==s[e]){A.warn(`DATERANGE tag attribute: "${e}" does not match for tags with ID: "${t.ID}"`),this._badValueForSameId=e;break}t=u(new k({}),s,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){const t=new Date(this.attr["END-DATE"]);f(t.getTime())&&(this._endDate=t)}}get id(){return this.attr.ID}get class(){return this.attr.CLASS}get startDate(){return this._startDate}get endDate(){if(this._endDate)return this._endDate;const t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}get duration(){if("DURATION"in this.attr){const t=this.attr.decimalFloatingPoint("DURATION");if(f(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}get plannedDuration(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}get endOnNext(){return this.attr.bool("END-ON-NEXT")}get isValid(){return!!this.id&&!this._badValueForSameId&&f(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}class I{constructor(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}}}var _="audio",C="video",x="audiovideo";class P{constructor(t){this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams={[_]:null,[C]:null,[x]:null},this.baseurl=t}setByteRange(t,e){const s=t.split("@",2);let i;i=1===s.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(s[1]),this._byteRange=[i,parseInt(s[0])+i]}get byteRange(){return this._byteRange?this._byteRange:[]}get byteRangeStartOffset(){return this.byteRange[0]}get byteRangeEndOffset(){return this.byteRange[1]}get url(){return!this._url&&this.baseurl&&this.relurl&&(this._url=o.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""}set url(t){this._url=t}}class M extends P{constructor(t,e){super(e),this._decryptdata=null,this.rawProgramDateTime=null,this.programDateTime=null,this.tagList=[],this.duration=0,this.sn=0,this.levelkeys=void 0,this.type=void 0,this.loader=null,this.keyLoader=null,this.level=-1,this.cc=0,this.startPTS=void 0,this.endPTS=void 0,this.startDTS=void 0,this.endDTS=void 0,this.start=0,this.deltaPTS=void 0,this.maxStartPTS=void 0,this.minEndPTS=void 0,this.stats=new I,this.data=void 0,this.bitrateTest=!1,this.title=null,this.initSegment=null,this.endList=void 0,this.gap=void 0,this.urlId=0,this.type=t}get decryptdata(){const{levelkeys:t}=this;if(!t&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){const t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{const t=Object.keys(this.levelkeys);if(1===t.length)return this._decryptdata=this.levelkeys[t[0]].getDecryptData(this.sn)}}return this._decryptdata}get end(){return this.start+this.duration}get endProgramDateTime(){if(null===this.programDateTime)return null;if(!f(this.programDateTime))return null;const t=f(this.duration)?this.duration:0;return this.programDateTime+1e3*t}get encrypted(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){const t=Object.keys(this.levelkeys),e=t.length;if(e>1||1===e&&this.levelkeys[t[0]].encrypted)return!0}return!1}setKeyFormat(t){if(this.levelkeys){const e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}}abortRequests(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()}setElementaryStreamInfo(t,e,s,i,r,n=!1){const{elementaryStreams:a}=this,o=a[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,s),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,r)):a[t]={startPTS:e,endPTS:s,startDTS:i,endDTS:r,partial:n}}clearElementaryStreamInfo(){const{elementaryStreams:t}=this;t[_]=null,t[C]=null,t[x]=null}}class F extends P{constructor(t,e,s,i,r){super(s),this.fragOffset=0,this.duration=0,this.gap=!1,this.independent=!1,this.relurl=void 0,this.fragment=void 0,this.index=void 0,this.stats=new I,this.duration=t.decimalFloatingPoint("DURATION"),this.gap=t.bool("GAP"),this.independent=t.bool("INDEPENDENT"),this.relurl=t.enumeratedString("URI"),this.fragment=e,this.index=i;const n=t.enumeratedString("BYTERANGE");n&&this.setByteRange(n,r),r&&(this.fragOffset=r.fragOffset+r.duration)}get start(){return this.fragment.start+this.fragOffset}get end(){return this.start+this.duration}get loaded(){const{elementaryStreams:t}=this;return!!(t.audio||t.video||t.audiovideo)}}class O{constructor(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}reloaded(t){if(!t)return this.advanced=!0,void(this.updated=!0);const e=this.lastPartSn-t.lastPartSn,s=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!s||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&s>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay}get hasProgramDateTime(){return!!this.fragments.length&&f(this.fragments[this.fragments.length-1].programDateTime)}get levelTargetDuration(){return this.averagetargetduration||this.targetduration||10}get drift(){const t=this.driftEndTime-this.driftStartTime;if(t>0){return 1e3*(this.driftEnd-this.driftStart)/t}return 1}get edge(){return this.partEnd||this.fragmentEnd}get partEnd(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}get fragmentEnd(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}get age(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}get lastPartIndex(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}get lastPartSn(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}function N(t){return Uint8Array.from(atob(t),(t=>t.charCodeAt(0)))}function U(t){const e=t.split(":");let s=null;if("data"===e[0]&&2===e.length){const t=e[1].split(";"),i=t[t.length-1].split(",");if(2===i.length){const e="base64"===i[0],r=i[1];e?(t.splice(-1,1),s=N(r)):s=function(t){const e=B(t).subarray(0,16),s=new Uint8Array(16);return s.set(e,16-e.length),s}(r)}}return s}function B(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(t=>t.charCodeAt(0)))}const $="undefined"!=typeof self?self:void 0;var G={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},K="org.w3.clearkey",H="com.apple.streamingkeydelivery",V="com.microsoft.playready",Y="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function W(t){switch(t){case H:return G.FAIRPLAY;case V:return G.PLAYREADY;case Y:return G.WIDEVINE;case K:return G.CLEARKEY}}var j="1077efecc0b24d02ace33c1e52e2fb4b",q="e2719d58a985b3c9781ab030af78d30e",X="9a04f07998404286ab92e65be0885f95",z="edef8ba979d64acea3c827dcd51d21ed";function Q(t){return t===z?G.WIDEVINE:t===X?G.PLAYREADY:t===j||t===q?G.CLEARKEY:void 0}function J(t){switch(t){case G.FAIRPLAY:return H;case G.PLAYREADY:return V;case G.WIDEVINE:return Y;case G.CLEARKEY:return K}}function Z(t){const{drmSystems:e,widevineLicenseUrl:s}=t,i=e?[G.FAIRPLAY,G.WIDEVINE,G.PLAYREADY,G.CLEARKEY].filter((t=>!!e[t])):[];return!i[G.WIDEVINE]&&s&&i.push(G.WIDEVINE),i}const tt=null!=$&&null!=(et=$.navigator)&&et.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;var et;function st(t,e,s){return Uint8Array.prototype.slice?t.slice(e,s):new Uint8Array(Array.prototype.slice.call(t,e,s))}const it=(t,e)=>e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128,rt=(t,e)=>e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128,nt=(t,e)=>{const s=e;let i=0;for(;it(t,e);){i+=10;i+=at(t,e+6),rt(t,e+10)&&(i+=10),e+=i}if(i>0)return t.subarray(s,s+i)},at=(t,e)=>{let s=0;return s=(127&t[e])<<21,s|=(127&t[e+1])<<14,s|=(127&t[e+2])<<7,s|=127&t[e+3],s},ot=(t,e)=>it(t,e)&&at(t,e+6)+10<=t.length-e,lt=t=>{const e=ct(t);for(let t=0;tt&&"PRIV"===t.key&&"com.apple.streaming.transportStreamTimestamp"===t.info,dt=t=>{const e=String.fromCharCode(t[0],t[1],t[2],t[3]),s=at(t,4);return{type:e,size:s,data:t.subarray(10,10+s)}},ct=t=>{let e=0;const s=[];for(;it(t,e);){const i=at(t,e+6);e+=10;const r=e+i;for(;e+8"PRIV"===t.type?ft(t):"W"===t.type[0]?mt(t):gt(t),ft=t=>{if(t.size<2)return;const e=vt(t.data,!0),s=new Uint8Array(t.data.subarray(e.length+1));return{key:t.type,info:e,data:s.buffer}},gt=t=>{if(t.size<2)return;if("TXXX"===t.type){let e=1;const s=vt(t.data.subarray(e),!0);e+=s.length+1;const i=vt(t.data.subarray(e));return{key:t.type,info:s,data:i}}const e=vt(t.data.subarray(1));return{key:t.type,data:e}},mt=t=>{if("WXXX"===t.type){if(t.size<2)return;let e=1;const s=vt(t.data.subarray(e),!0);e+=s.length+1;const i=vt(t.data.subarray(e));return{key:t.type,info:s,data:i}}const e=vt(t.data);return{key:t.type,data:e}},pt=t=>{if(8===t.data.byteLength){const e=new Uint8Array(t.data),s=1&e[3];let i=(e[4]<<23)+(e[5]<<15)+(e[6]<<7)+e[7];return i/=45,s&&(i+=47721858.84),Math.round(i)}},vt=(t,e=!1)=>{const s=Et();if(s){const i=s.decode(t);if(e){const t=i.indexOf("\0");return-1!==t?i.substring(0,t):i}return i.replace(/\0/g,"")}const i=t.length;let r,n,a,o="",l=0;for(;l>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:o+=String.fromCharCode(r);break;case 12:case 13:n=t[l++],o+=String.fromCharCode((31&r)<<6|63&n);break;case 14:n=t[l++],a=t[l++],o+=String.fromCharCode((15&r)<<12|(63&n)<<6|63&a)}}return o};let yt;function Et(){if(!navigator.userAgent.includes("PlayStation 4"))return yt||void 0===self.TextDecoder||(yt=new self.TextDecoder("utf-8")),yt}const Tt=function(t){let e="";for(let s=0;s>24,t[e+1]=s>>16&255,t[e+2]=s>>8&255,t[e+3]=255&s}function _t(t,e){const s=[];if(!e.length)return s;const i=t.byteLength;for(let r=0;r1?r+n:i;if(Rt(t.subarray(r+4,r+8))===e[0])if(1===e.length)s.push(t.subarray(r+8,a));else{const i=_t(t.subarray(r+8,a),e.slice(1));i.length&&Lt.apply(s,i)}r=a}return s}function Ct(t){const e=[],s=t[0];let i=8;const r=kt(t,i);i+=4;let n=0,a=0;0===s?(n=kt(t,i),a=kt(t,i+4),i+=8):(n=wt(t,i),a=wt(t,i+8),i+=16),i+=2;let o=t.length+a;const l=bt(t,i);i+=2;for(let s=0;s>>31)return A.warn("SIDX has hierarchical references (not supported)"),null;const l=kt(t,s);s+=4,e.push({referenceSize:a,subsegmentDuration:l,info:{duration:l/r,start:o,end:o+a-1}}),o+=a,s+=4,i=s}return{earliestPresentationTime:n,timescale:r,version:s,referencesCount:l,references:e}}function xt(t){const e=[],s=_t(t,["moov","trak"]);for(let t=0;t{const s=kt(t,4),i=e[s];i&&(i.default={duration:kt(t,12),flags:kt(t,20)})})),e}function Pt(t){const e=t.subarray(8),s=e.subarray(86),i=Rt(e.subarray(4,8));let r=i;const n="enca"===i||"encv"===i;if(n){const t=_t(e,[i])[0];_t(t.subarray("enca"===i?28:78),["sinf"]).forEach((t=>{const e=_t(t,["schm"])[0];if(e){const s=Rt(e.subarray(4,8));if("cbcs"===s||"cenc"===s){const e=_t(t,["frma"])[0];e&&(r=Rt(e))}}}))}switch(r){case"avc1":case"avc2":case"avc3":case"avc4":{const t=_t(s,["avcC"])[0];r+="."+Ft(t[1])+Ft(t[2])+Ft(t[3]);break}case"mp4a":{const t=_t(e,[i])[0],s=_t(t.subarray(28),["esds"])[0];if(s&&s.length>12){let t=4;if(3!==s[t++])break;t=Mt(s,t),t+=2;const e=s[t++];if(128&e&&(t+=2),64&e&&(t+=s[t++]),4!==s[t++])break;t=Mt(s,t);const i=s[t++];if(64!==i)break;if(r+="."+Ft(i),t+=12,5!==s[t++])break;t=Mt(s,t);const n=s[t++];let a=(248&n)>>3;31===a&&(a+=1+((7&n)<<3)+((224&s[t])>>5)),r+="."+a}break}case"hvc1":case"hev1":{const t=_t(s,["hvcC"])[0],e=t[1],i=["","A","B","C"][e>>6],n=31&e,a=kt(t,2),o=(32&e)>>5?"H":"L",l=t[12],h=t.subarray(6,12);r+="."+i+n,r+="."+a.toString(16).toUpperCase(),r+="."+o+l;let d="";for(let t=h.length;t--;){const e=h[t];if(e||d){d="."+e.toString(16).toUpperCase()+d}}r+=d;break}case"dvh1":case"dvhe":{const t=_t(s,["dvcC"])[0],e=t[2]>>1&127,i=t[2]<<5&32|t[3]>>3&31;r+="."+Ot(e)+"."+Ot(i);break}case"vp09":{const t=_t(s,["vpcC"])[0],e=t[4],i=t[5],n=t[6]>>4&15;r+="."+Ot(e)+"."+Ot(i)+"."+Ot(n);break}case"av01":{const t=_t(s,["av1C"])[0],e=t[1]>>>5,i=31&t[1],n=t[2]>>>7?"H":"M",a=(64&t[2])>>6,o=(32&t[2])>>5,l=2===e&&a?o?12:10:a?10:8,h=(16&t[2])>>4,d=(8&t[2])>>3,c=(4&t[2])>>2,u=3&t[2],f=1,g=1,m=1,p=0;r+="."+e+"."+Ot(i)+n+"."+Ot(l)+"."+h+"."+d+c+u+"."+Ot(f)+"."+Ot(g)+"."+Ot(m)+"."+p;break}}return{codec:r,encrypted:n}}function Mt(t,e){const s=e+5;for(;128&t[e++]&&e{const l=o.byteOffset-8;_t(o,["traf"]).map((o=>{const h=_t(o,["tfdt"]).map((t=>{const e=t[0];let s=kt(t,4);return 1===e&&(s*=Math.pow(2,32),s+=kt(t,8)),s/r}))[0];return void 0!==h&&(t=h),_t(o,["tfhd"]).map((h=>{const d=kt(h,4),c=16777215&kt(h,0);let u=0;const f=!!(16&c);let g=0;const m=!!(32&c);let p=8;d===n&&(!!(1&c)&&(p+=8),!!(2&c)&&(p+=4),!!(8&c)&&(u=kt(h,p),p+=4),f&&(g=kt(h,p),p+=4),m&&(p+=4),"video"===e.type&&(a=function(t){if(!t)return!1;const e=t.indexOf("."),s=e<0?t:t.substring(0,e);return"hvc1"===s||"hev1"===s||"dvh1"===s||"dvhe"===s}(e.codec)),_t(o,["trun"]).map((n=>{const o=n[0],h=16777215&kt(n,0),d=!!(1&h);let c=0;const f=!!(4&h),m=!!(256&h);let p=0;const v=!!(512&h);let y=0;const E=!!(1024&h),T=!!(2048&h);let S=0;const L=kt(n,4);let A=8;d&&(c=kt(n,A),A+=4),f&&(A+=4);let R=c+l;for(let l=0;l>1&63;return 39===t||40===t}return 6===(31&e)}function Kt(t,e,s,i){const r=Ht(t);let n=0;n+=e;let a=0,o=0,l=0;for(;n=r.length)break;l=r[n++],a+=l}while(255===l);o=0;do{if(n>=r.length)break;l=r[n++],o+=l}while(255===l);const t=r.length-n;let e=n;if(ot){A.error(`Malformed SEI payload. ${o} is too small, only ${t} bytes left to parse.`);break}if(4===a){if(181===r[e++]){const t=bt(r,e);if(e+=2,49===t){const t=kt(r,e);if(e+=4,1195456820===t){const t=r[e++];if(3===t){const n=r[e++],o=64&n,l=o?2+3*(31&n):0,h=new Uint8Array(l);if(o){h[0]=n;for(let t=1;t16){const t=[];for(let s=0;s<16;s++){const i=r[e++].toString(16);t.push(1==i.length?"0"+i:i),3!==s&&5!==s&&7!==s&&9!==s||t.push("-")}const n=o-16,l=new Uint8Array(n);for(let t=0;t0?(n=new Uint8Array(4),e.length>0&&new DataView(n.buffer).setUint32(0,e.length,!1)):n=new Uint8Array;const a=new Uint8Array(4);return s&&s.byteLength>0&&new DataView(a.buffer).setUint32(0,s.byteLength,!1),function(t,...e){const s=e.length;let i=8,r=s;for(;r--;)i+=e[r].byteLength;const n=new Uint8Array(i);for(n[0]=i>>24&255,n[1]=i>>16&255,n[2]=i>>8&255,n[3]=255&i,n.set(t,4),r=0,i=8;r>>24;if(0!==r&&1!==r)return{offset:s,size:e};const n=t.buffer,a=Tt(new Uint8Array(n,s+12,16)),o=t.getUint32(28);let l=null,h=null;if(0===r){if(e-32>8*(15-s)&255;return e}(t);return new jt(this.method,this.uri,"identity",this.keyFormatVersions,e)}const e=U(this.uri);if(e)switch(this.keyFormat){case Y:this.pssh=e,e.length>=22&&(this.keyId=e.subarray(e.length-22,e.length-6));break;case V:{const t=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Vt(t,null,e);const s=new Uint16Array(e.buffer,e.byteOffset,e.byteLength/2),i=String.fromCharCode.apply(null,Array.from(s)),r=i.substring(i.indexOf("<"),i.length),n=(new DOMParser).parseFromString(r,"text/xml").getElementsByTagName("KID")[0];if(n){const t=n.childNodes[0]?n.childNodes[0].nodeValue:n.getAttribute("VALUE");if(t){const e=N(t).subarray(0,16);!function(t){const e=function(t,e,s){const i=t[e];t[e]=t[s],t[s]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(e),this.keyId=e}}break}default:{let t=e.subarray(0,16);if(16!==t.length){const e=new Uint8Array(16);e.set(t,16-t.length),t=e}this.keyId=t;break}}if(!this.keyId||16!==this.keyId.byteLength){let t=Wt[this.uri];if(!t){const e=Object.keys(Wt).length%Number.MAX_SAFE_INTEGER;t=new Uint8Array(16);new DataView(t.buffer,12,4).setUint32(0,e),Wt[this.uri]=t}this.keyId=t}return this}}const qt=/\{\$([a-zA-Z0-9-_]+)\}/g;function Xt(t){return qt.test(t)}function zt(t,e,s){if(null!==t.variableList||t.hasVariableRefs)for(let i=s.length;i--;){const r=s[i],n=e[r];n&&(e[r]=Qt(t,n))}}function Qt(t,e){if(null!==t.variableList||t.hasVariableRefs){const s=t.variableList;return e.replace(qt,(e=>{const i=e.substring(2,e.length-1),r=null==s?void 0:s[i];return void 0===r?(t.playlistParsingError||(t.playlistParsingError=new Error(`Missing preceding EXT-X-DEFINE tag for Variable Reference: "${i}"`)),e):r}))}return e}function Jt(t,e,s){let i,r,n=t.variableList;if(n||(t.variableList=n={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{const t=new self.URL(s).searchParams;if(!t.has(i))throw new Error(`"${i}" does not match any query parameter in URI: "${s}"`);r=t.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE QUERYPARAM: ${e.message}`))}}else i=e.NAME,r=e.VALUE;i in n?t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE duplicate Variable Name declarations: "${i}"`)):n[i]=r||""}function Zt(t,e,s){const i=e.IMPORT;if(s&&i in s){let e=t.variableList;e||(t.variableList=e={}),e[i]=s[i]}else t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "${i}"`))}function te(t=!0){if("undefined"==typeof self)return;return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}const ee={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function se(t,e,s=!0){return!t.split(",").some((t=>!ie(t,e,s)))}function ie(t,e,s=!0){var i;const r=te(s);return null!=(i=null==r?void 0:r.isTypeSupported(re(t,e)))&&i}function re(t,e){return`${e}/mp4;codecs="${t}"`}function ne(t){if(t){const e=t.substring(0,4);return ee.video[e]}return 2}function ae(t){return t.split(",").reduce(((t,e)=>{const s=ee.video[e];return s?(2*s+t)/(t?3:2):(ee.audio[e]+t)/(t?2:1)}),0)}const oe={};const le=/flac|opus/i;function he(t,e=!0){return t.replace(le,(t=>function(t,e=!0){if(oe[t])return oe[t];const s={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t];for(let i=0;i0&&i.length({id:t.attrs.AUDIO,audioCodec:t.audioCodec}))),SUBTITLES:n.map((t=>({id:t.attrs.SUBTITLES,textCodec:t.textCodec}))),"CLOSED-CAPTIONS":[]};let o=0;for(ue.lastIndex=0;null!==(i=ue.exec(t));){const t=new k(i[1]),n=t.TYPE;if(n){const i=a[n],l=r[n]||[];r[n]=l,zt(s,t,["URI","GROUP-ID","LANGUAGE","ASSOC-LANGUAGE","STABLE-RENDITION-ID","NAME","INSTREAM-ID","CHARACTERISTICS","CHANNELS"]);const h=t.LANGUAGE,d=t["ASSOC-LANGUAGE"],c=t.CHANNELS,u=t.CHARACTERISTICS,f=t["INSTREAM-ID"],g={attrs:t,bitrate:0,id:o++,groupId:t["GROUP-ID"]||"",name:t.NAME||h||"",type:n,default:t.bool("DEFAULT"),autoselect:t.bool("AUTOSELECT"),forced:t.bool("FORCED"),lang:h,url:t.URI?pe.resolve(t.URI,e):""};if(d&&(g.assocLang=d),c&&(g.channels=c),u&&(g.characteristics=u),f&&(g.instreamId=f),null!=i&&i.length){const t=pe.findGroup(i,g.groupId)||i[0];Te(g,t,"audioCodec"),Te(g,t,"textCodec")}l.push(g)}}return r}static parseLevelPlaylist(t,e,s,i,r,n){const a=new O(e),o=a.fragments;let l,h,d,c=null,g=0,m=0,p=0,v=0,y=null,E=new M(i,e),T=-1,S=!1,L=null;for(ge.lastIndex=0,a.m3u8=t,a.hasVariableRefs=Xt(t);null!==(l=ge.exec(t));){S&&(S=!1,E=new M(i,e),E.start=p,E.sn=g,E.cc=v,E.level=s,c&&(E.initSegment=c,E.rawProgramDateTime=c.rawProgramDateTime,c.rawProgramDateTime=null,L&&(E.setByteRange(L),L=null)));const t=l[1];if(t){E.duration=parseFloat(t);const e=(" "+l[2]).slice(1);E.title=e||null,E.tagList.push(e?["INF",t,e]:["INF",t])}else if(l[3]){if(f(E.duration)){E.start=p,d&&Ae(E,d,a),E.sn=g,E.level=s,E.cc=v,o.push(E);const t=(" "+l[3]).slice(1);E.relurl=Qt(a,t),Se(E,y),y=E,p+=E.duration,g++,m=0,S=!0}}else if(l[4]){const t=(" "+l[4]).slice(1);y?E.setByteRange(t,y):E.setByteRange(t)}else if(l[5])E.rawProgramDateTime=(" "+l[5]).slice(1),E.tagList.push(["PROGRAM-DATE-TIME",E.rawProgramDateTime]),-1===T&&(T=o.length);else{if(l=l[0].match(me),!l){A.warn("No matches on slow regex match for level playlist!");continue}for(h=1;h0&&t.bool("CAN-SKIP-DATERANGES"),a.partHoldBack=t.optionalFloat("PART-HOLD-BACK",0),a.holdBack=t.optionalFloat("HOLD-BACK",0);break}case"PART-INF":{const t=new k(r);a.partTarget=t.decimalFloatingPoint("PART-TARGET");break}case"PART":{let t=a.partList;t||(t=a.partList=[]);const s=m>0?t[t.length-1]:void 0,i=m++,n=new k(r);zt(a,n,["BYTERANGE","URI"]);const o=new F(n,E,e,i,s);t.push(o),E.duration+=o.duration;break}case"PRELOAD-HINT":{const t=new k(r);zt(a,t,["URI"]),a.preloadHint=t;break}case"RENDITION-REPORT":{const t=new k(r);zt(a,t,["URI"]),a.renditionReports=a.renditionReports||[],a.renditionReports.push(t);break}default:A.warn(`line parsed but not handled: ${l}`)}}}y&&!y.relurl?(o.pop(),p-=y.duration,a.partList&&(a.fragmentHint=y)):a.partList&&(Se(E,y),E.cc=v,a.fragmentHint=E,d&&Ae(E,d,a));const R=o.length,b=o[0],w=o[R-1];if(p+=a.skippedSegments*a.targetduration,p>0&&R&&w){a.averagetargetduration=p/R;const t=w.sn;a.endSN="initSegment"!==t?t:0,a.live||(w.endList=!0),b&&(a.startCC=b.cc)}else a.endSN=0,a.startCC=0;return a.fragmentHint&&(p+=a.fragmentHint.duration),a.totalduration=p,a.endCC=v,T>0&&function(t,e){let s=t[e];for(let i=e;i--;){const e=t[i];if(!e)return;e.programDateTime=s.programDateTime-1e3*e.duration,s=e}}(o,T),a}}function ve(t,e,s){var i,r;const n=new k(t);zt(s,n,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);const a=null!=(i=n.METHOD)?i:"",o=n.URI,l=n.hexadecimalInteger("IV"),h=n.KEYFORMATVERSIONS,d=null!=(r=n.KEYFORMAT)?r:"identity";o&&n.IV&&!l&&A.error(`Invalid IV: ${n.IV}`);const c=o?pe.resolve(o,e):"",u=(h||"1").split("/").map(Number).filter(Number.isFinite);return new jt(a,c,d,u,l)}function ye(t){const e=new k(t).decimalFloatingPoint("TIME-OFFSET");return f(e)?e:null}function Ee(t,e){let s=(t||"").split(/[ ,]+/).filter((t=>t));["video","audio","text"].forEach((t=>{const i=s.filter((e=>function(t,e){const s=ee[e];return!!s&&!!s[t.slice(0,4)]}(e,t)));i.length&&(e[`${t}Codec`]=i.join(","),s=s.filter((t=>-1===i.indexOf(t))))})),e.unknownCodecs=s}function Te(t,e,s){const i=e[s];i&&(t[s]=i)}function Se(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),f(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Le(t,e,s,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=s,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function Ae(t,e,s){t.levelkeys=e;const{encryptedFragments:i}=s;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((t=>e[t].isCommonEncryption))||i.push(t)}var Re="manifest",be="level",ke="audioTrack",we="subtitleTrack",De="main",Ie="audio",_e="subtitle";function Ce(t){const{type:e}=t;switch(e){case ke:return Ie;case we:return _e;default:return De}}function xe(t,e){let s=t.url;return void 0!==s&&0!==s.indexOf("data:")||(s=e.url),s}class Pe{constructor(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}startLoad(t){}stopLoad(){this.destroyInternalLoaders()}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(p.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(p.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)}createInternalLoader(t){const e=this.hls.config,s=e.pLoader,i=e.loader,r=new(s||i)(e);return this.loaders[t.type]=r,r}getInternalLoader(t){return this.loaders[t.type]}resetInternalLoader(t){this.loaders[t]&&delete this.loaders[t]}destroyInternalLoaders(){for(const t in this.loaders){const e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}}destroy(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()}onManifestLoading(t,e){const{url:s}=e;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Re,url:s,deliveryDirectives:null})}onLevelLoading(t,e){const{id:s,level:i,pathwayId:r,url:n,deliveryDirectives:a}=e;this.load({id:s,level:i,pathwayId:r,responseType:"text",type:be,url:n,deliveryDirectives:a})}onAudioTrackLoading(t,e){const{id:s,groupId:i,url:r,deliveryDirectives:n}=e;this.load({id:s,groupId:i,level:null,responseType:"text",type:ke,url:r,deliveryDirectives:n})}onSubtitleTrackLoading(t,e){const{id:s,groupId:i,url:r,deliveryDirectives:n}=e;this.load({id:s,groupId:i,level:null,responseType:"text",type:we,url:r,deliveryDirectives:n})}load(t){var e;const s=this.hls.config;let i,r=this.getInternalLoader(t);if(r){const e=r.context;if(e&&e.url===t.url&&e.level===t.level)return void A.trace("[playlist-loader]: playlist request ongoing");A.log(`[playlist-loader]: aborting previous loader for type: ${t.type}`),r.abort()}if(i=t.type===Re?s.manifestLoadPolicy.default:u({},s.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),r=this.createInternalLoader(t),f(null==(e=t.deliveryDirectives)?void 0:e.part)){let e;if(t.type===be&&null!==t.level?e=this.hls.levels[t.level].details:t.type===ke&&null!==t.id?e=this.hls.audioTracks[t.id].details:t.type===we&&null!==t.id&&(e=this.hls.subtitleTracks[t.id].details),e){const t=e.partTarget,s=e.targetduration;if(t&&s){const e=1e3*Math.max(3*t,.8*s);i=u({},i,{maxTimeToFirstByteMs:Math.min(e,i.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(e,i.maxTimeToFirstByteMs)})}}}const n=i.errorRetry||i.timeoutRetry||{},a={loadPolicy:i,timeout:i.maxLoadTimeMs,maxRetry:n.maxNumRetry||0,retryDelay:n.retryDelayMs||0,maxRetryDelay:n.maxRetryDelayMs||0},o={onSuccess:(t,e,s,i)=>{const r=this.getInternalLoader(s);this.resetInternalLoader(s.type);const n=t.data;0===n.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),pe.isMediaPlaylist(n)?this.handleTrackOrLevelPlaylist(t,e,s,i||null,r):this.handleMasterPlaylist(t,e,s,i)):this.handleManifestParsingError(t,s,new Error("no EXTM3U delimiter"),i||null,e)},onError:(t,e,s,i)=>{this.handleNetworkError(e,s,!1,t,i)},onTimeout:(t,e,s)=>{this.handleNetworkError(e,s,!0,void 0,t)}};r.load(t,a,o)}handleMasterPlaylist(t,e,s,i){const r=this.hls,n=t.data,a=xe(t,s),o=pe.parseMasterPlaylist(n,a);if(o.playlistParsingError)return void this.handleManifestParsingError(t,s,o.playlistParsingError,i,e);const{contentSteering:l,levels:h,sessionData:d,sessionKeys:c,startTimeOffset:u,variableList:f}=o;this.variableList=f;const{AUDIO:g=[],SUBTITLES:m,"CLOSED-CAPTIONS":v}=pe.parseMasterPlaylistMedia(n,a,o);if(g.length){g.some((t=>!t.url))||!h[0].audioCodec||h[0].attrs.AUDIO||(A.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),g.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new k({}),bitrate:0,url:""}))}r.trigger(p.MANIFEST_LOADED,{levels:h,audioTracks:g,subtitles:m,captions:v,contentSteering:l,url:a,stats:e,networkDetails:i,sessionData:d,sessionKeys:c,startTimeOffset:u,variableList:f})}handleTrackOrLevelPlaylist(t,e,s,i,r){const n=this.hls,{id:a,level:o,type:l}=s,h=xe(t,s),d=f(o)?o:f(a)?a:0,c=Ce(s),u=pe.parseLevelPlaylist(t.data,h,d,c,0,this.variableList);if(l===Re){const t={attrs:new k({}),bitrate:0,details:u,name:"",url:h};n.trigger(p.MANIFEST_LOADED,{levels:[t],audioTracks:[],url:h,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),s.levelDetails=u,this.handlePlaylistLoaded(u,t,e,s,i,r)}handleManifestParsingError(t,e,s,i,r){this.hls.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.MANIFEST_PARSING_ERROR,fatal:e.type===Re,url:t.url,err:s,error:s,reason:s.message,response:t,context:e,networkDetails:i,stats:r})}handleNetworkError(t,e,s=!1,i,r){let n=`A network ${s?"timeout":"error"+(i?" (status "+i.code+")":"")} occurred while loading ${t.type}`;t.type===be?n+=`: ${t.level} id: ${t.id}`:t.type!==ke&&t.type!==we||(n+=` id: ${t.id} group-id: "${t.groupId}"`);const a=new Error(n);A.warn(`[playlist-loader]: ${n}`);let o=y.UNKNOWN,l=!1;const d=this.getInternalLoader(t);switch(t.type){case Re:o=s?y.MANIFEST_LOAD_TIMEOUT:y.MANIFEST_LOAD_ERROR,l=!0;break;case be:o=s?y.LEVEL_LOAD_TIMEOUT:y.LEVEL_LOAD_ERROR,l=!1;break;case ke:o=s?y.AUDIO_TRACK_LOAD_TIMEOUT:y.AUDIO_TRACK_LOAD_ERROR,l=!1;break;case we:o=s?y.SUBTITLE_TRACK_LOAD_TIMEOUT:y.SUBTITLE_LOAD_ERROR,l=!1}d&&this.resetInternalLoader(t.type);const c={type:v.NETWORK_ERROR,details:o,fatal:l,url:t.url,loader:d,context:t,error:a,networkDetails:e,stats:r};if(i){const s=(null==e?void 0:e.url)||t.url;c.response=h({url:s,data:void 0},i)}this.hls.trigger(p.ERROR,c)}handlePlaylistLoaded(t,e,s,i,r,n){const a=this.hls,{type:o,level:l,id:h,groupId:d,deliveryDirectives:c}=i,u=xe(e,i),f=Ce(i),g="number"==typeof i.level&&f===De?l:void 0;if(!t.fragments.length){const t=new Error("No Segments found in Playlist");return void a.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.LEVEL_EMPTY_ERROR,fatal:!1,url:u,error:t,reason:t.message,response:e,context:i,level:g,parent:f,networkDetails:r,stats:s})}t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));const m=t.playlistParsingError;if(m)a.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.LEVEL_PARSING_ERROR,fatal:!1,url:u,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:r,stats:s});else switch(t.live&&n&&(n.getCacheAge&&(t.ageHeader=n.getCacheAge()||0),n.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Re:case be:a.trigger(p.LEVEL_LOADED,{details:t,level:g||0,id:h||0,stats:s,networkDetails:r,deliveryDirectives:c});break;case ke:a.trigger(p.AUDIO_TRACK_LOADED,{details:t,id:h||0,groupId:d||"",stats:s,networkDetails:r,deliveryDirectives:c});break;case we:a.trigger(p.SUBTITLE_TRACK_LOADED,{details:t,id:h||0,groupId:d||"",stats:s,networkDetails:r,deliveryDirectives:c})}}}function Me(t,e){let s;try{s=new Event("addtrack")}catch(t){s=document.createEvent("Event"),s.initEvent("addtrack",!1,!1)}s.track=t,e.dispatchEvent(s)}function Fe(t,e){const s=t.mode;if("disabled"===s&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error(`addCue is failed for: ${e}`)}catch(s){A.debug(`[texttrack-utils]: ${s}`);try{const s=new self.TextTrackCue(e.startTime,e.endTime,e.text);s.id=e.id,t.addCue(s)}catch(t){A.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${t}`)}}"disabled"===s&&(t.mode=s)}function Oe(t){const e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(let e=t.cues.length;e--;)t.removeCue(t.cues[e]);"disabled"===e&&(t.mode=e)}function Ne(t,e,s,i){const r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&t.cues.length>0){const r=function(t,e,s){const i=[],r=function(t,e){if(et[s].endTime)return-1;let i=0,r=s;for(;i<=r;){const n=Math.floor((r+i)/2);if(et[n].startTime&&i-1)for(let n=r,a=t.length;n=e&&r.endTime<=s)i.push(r);else if(r.startTime>s)return i}return i}(t.cues,e,s);for(let e=0;e{const t=Ke();try{t&&new t(0,Number.POSITIVE_INFINITY,"")}catch(t){return Number.MAX_VALUE}return Number.POSITIVE_INFINITY})();function Ye(t,e){return t.getTime()/1e3-e}class We{constructor(t){this.hls=void 0,this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.hls=t,this._registerListeners()}destroy(){this._unregisterListeners(),this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.hls=null}_registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this)}onMediaAttached(t,e){this.media=e.media}onMediaDetaching(){this.id3Track&&(Oe(this.id3Track),this.id3Track=null,this.media=null,this.dateRangeCuesAppended={})}onManifestLoading(){this.dateRangeCuesAppended={}}createTrack(t){const e=this.getID3Track(t.textTracks);return e.mode="hidden",e}getID3Track(t){if(this.media){for(let e=0;eVe&&(i=Ve);i-s<=0&&(i=s+.25);for(let t=0;tt.type===Be&&o:"video"===i?t=>t.type===Ge&&a:t=>t.type===Be&&o||t.type===Ge&&a,Ne(r,e,s,t)}}onLevelUpdated(t,{details:e}){if(!this.media||!e.hasProgramDateTime||!this.hls.config.enableDateRangeMetadataCues)return;const{dateRangeCuesAppended:s,id3Track:i}=this,{dateRanges:r}=e,n=Object.keys(r);if(i){const t=Object.keys(s).filter((t=>!n.includes(t)));for(let e=t.length;e--;){const r=t[e];Object.keys(s[r].cues).forEach((t=>{i.removeCue(s[r].cues[t])})),delete s[r]}}const a=e.fragments[e.fragments.length-1];if(0===n.length||!f(null==a?void 0:a.programDateTime))return;this.id3Track||(this.id3Track=this.createTrack(this.media));const o=a.programDateTime/1e3-a.start,l=Ke();for(let t=0;t{if(e!==i.id){const s=r[e];if(s.class===i.class&&s.startDate>i.startDate&&(!t||i.startDatethis.timeupdate(),this.hls=t,this.config=t.config,this.registerListeners()}get latency(){return this._latency||0}get maxLatency(){const{config:t,levelDetails:e}=this;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}get targetLatency(){const{levelDetails:t}=this;if(null===t)return null;const{holdBack:e,partHoldBack:s,targetduration:i}=t,{liveSyncDuration:r,liveSyncDurationCount:n,lowLatencyMode:a}=this.config,o=this.hls.userConfig;let l=a&&s||e;(o.liveSyncDuration||o.liveSyncDurationCount||0===l)&&(l=void 0!==r?r:n*i);const h=i;return l+Math.min(1*this.stallCount,h)}get liveSyncPosition(){const t=this.estimateLiveEdge(),e=this.targetLatency,s=this.levelDetails;if(null===t||null===e||null===s)return null;const i=s.edge,r=t-e-this.edgeStalled,n=i-s.totalduration,a=i-(this.config.lowLatencyMode&&s.partTarget||s.targetduration);return Math.min(Math.max(n,r),a)}get drift(){const{levelDetails:t}=this;return null===t?1:t.drift}get edgeStalled(){const{levelDetails:t}=this;if(null===t)return 0;const e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}get forwardBufferLength(){const{media:t,levelDetails:e}=this;if(!t||!e)return 0;const s=t.buffered.length;return(s?t.buffered.end(s-1):e.edge)-this.currentTime}destroy(){this.unregisterListeners(),this.onMediaDetaching(),this.levelDetails=null,this.hls=this.timeupdateHandler=null}registerListeners(){this.hls.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),this.hls.on(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(p.LEVEL_UPDATED,this.onLevelUpdated,this),this.hls.on(p.ERROR,this.onError,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),this.hls.off(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(p.LEVEL_UPDATED,this.onLevelUpdated,this),this.hls.off(p.ERROR,this.onError,this)}onMediaAttached(t,e){this.media=e.media,this.media.addEventListener("timeupdate",this.timeupdateHandler)}onMediaDetaching(){this.media&&(this.media.removeEventListener("timeupdate",this.timeupdateHandler),this.media=null)}onManifestLoading(){this.levelDetails=null,this._latency=null,this.stallCount=0}onLevelUpdated(t,{details:e}){this.levelDetails=e,e.advanced&&this.timeupdate(),!e.live&&this.media&&this.media.removeEventListener("timeupdate",this.timeupdateHandler)}onError(t,e){var s;e.details===y.BUFFER_STALLED_ERROR&&(this.stallCount++,null!=(s=this.levelDetails)&&s.live&&A.warn("[playback-rate-controller]: Stall detected, adjusting target latency"))}timeupdate(){const{media:t,levelDetails:e}=this;if(!t||!e)return;this.currentTime=t.currentTime;const s=this.computeLatency();if(null===s)return;this._latency=s;const{lowLatencyMode:i,maxLiveSyncPlaybackRate:r}=this.config;if(!i||1===r||!e.live)return;const n=this.targetLatency;if(null===n)return;const a=s-n;if(a.05&&this.forwardBufferLength>1){const e=Math.min(2,Math.max(1,r)),s=Math.round(2/(1+Math.exp(-.75*a-this.edgeStalled))*20)/20;t.playbackRate=Math.min(e,Math.max(1,s))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}estimateLiveEdge(){const{levelDetails:t}=this;return null===t?null:t.edge+t.age}computeLatency(){const t=this.estimateLiveEdge();return null===t?null:t-this.currentTime}}const qe=["NONE","TYPE-0","TYPE-1",null];const Xe=["SDR","PQ","HLG"];var ze="",Qe="YES",Je="v2";function Ze(t){const{canSkipUntil:e,canSkipDateRanges:s,age:i}=t;return e&&i!!t)).map((t=>t.substring(0,4))).join(","),this.addGroupId("audio",t.attrs.AUDIO),this.addGroupId("text",t.attrs.SUBTITLES)}get maxBitrate(){return Math.max(this.realBitrate,this.bitrate)}get averageBitrate(){return this._avgBitrate||this.realBitrate||this.bitrate}get attrs(){return this._attrs[0]}get codecs(){return this.attrs.CODECS||""}get pathwayId(){return this.attrs["PATHWAY-ID"]||"."}get videoRange(){return this.attrs["VIDEO-RANGE"]||"SDR"}get score(){return this.attrs.optionalFloat("SCORE",0)}get uri(){return this.url[0]||""}hasAudioGroup(t){return ss(this._audioGroups,t)}hasSubtitleGroup(t){return ss(this._subtitleGroups,t)}get audioGroups(){return this._audioGroups}get subtitleGroups(){return this._subtitleGroups}addGroupId(t,e){if(e)if("audio"===t){let t=this._audioGroups;t||(t=this._audioGroups=[]),-1===t.indexOf(e)&&t.push(e)}else if("text"===t){let t=this._subtitleGroups;t||(t=this._subtitleGroups=[]),-1===t.indexOf(e)&&t.push(e)}}get urlId(){return 0}set urlId(t){}get audioGroupIds(){return this.audioGroups?[this.audioGroupId]:void 0}get textGroupIds(){return this.subtitleGroups?[this.textGroupId]:void 0}get audioGroupId(){var t;return null==(t=this.audioGroups)?void 0:t[0]}get textGroupId(){var t;return null==(t=this.subtitleGroups)?void 0:t[0]}addFallback(){}}function ss(t,e){return!(!e||!t)&&-1!==t.indexOf(e)}function is(t,e){const s=e.startPTS;if(f(s)){let i,r=0;e.sn>t.sn?(r=s-t.start,i=t):(r=t.start-s,i=e),i.duration!==r&&(i.duration=r)}else if(e.sn>t.sn){t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration}else e.start=Math.max(t.start-e.duration,0)}function rs(t,e,s,i,r,n){i-s<=0&&(A.warn("Fragment should have a positive duration",e),i=s+e.duration,n=r+e.duration);let a=s,o=i;const l=e.startPTS,h=e.endPTS;if(f(l)){const t=Math.abs(l-s);f(e.deltaPTS)?e.deltaPTS=Math.max(t,e.deltaPTS):e.deltaPTS=t,a=Math.max(s,l),s=Math.min(s,l),r=Math.min(r,e.startDTS),o=Math.min(i,h),i=Math.max(i,h),n=Math.max(n,e.endDTS)}const d=s-e.start;0!==e.start&&(e.start=s),e.duration=i-e.start,e.startPTS=s,e.maxStartPTS=a,e.startDTS=r,e.endPTS=i,e.minEndPTS=o,e.endDTS=n;const c=e.sn;if(!t||ct.endSN)return 0;let u;const g=c-t.startSN,m=t.fragments;for(m[g]=e,u=g;u>0;u--)is(m[u],m[u-1]);for(u=g;u=0;t--){const e=i[t].initSegment;if(e){s=e;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;let r,n=0;if(function(t,e,s){const i=e.skippedSegments,r=Math.max(t.startSN,e.startSN)-e.startSN,n=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,a=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments;for(let t=r;t<=n;t++){const r=l[a+t];let n=o[t];i&&!n&&t{t.relurl&&(n=t.cc-i.cc),f(t.startPTS)&&f(t.endPTS)&&(i.start=i.startPTS=t.startPTS,i.startDTS=t.startDTS,i.maxStartPTS=t.maxStartPTS,i.endPTS=t.endPTS,i.endDTS=t.endDTS,i.minEndPTS=t.minEndPTS,i.duration=t.endPTS-t.startPTS,i.duration&&(r=i),e.PTSKnown=e.alignedSliding=!0),i.elementaryStreams=t.elementaryStreams,i.loader=t.loader,i.stats=t.stats,t.initSegment&&(i.initSegment=t.initSegment,s=t.initSegment)})),s){(e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments).forEach((t=>{var e;!t||t.initSegment&&t.initSegment.relurl!==(null==(e=s)?void 0:e.relurl)||(t.initSegment=s)}))}if(e.skippedSegments)if(e.deltaUpdateFailed=e.fragments.some((t=>!t)),e.deltaUpdateFailed){A.warn("[level-helper] Previous playlist missing segments skipped in delta playlist");for(let t=e.skippedSegments;t--;)e.fragments.shift();e.startSN=e.fragments[0].sn,e.startCC=e.fragments[0].cc}else e.canSkipDateRanges&&(e.dateRanges=function(t,e,s){const i=u({},t);s&&s.forEach((t=>{delete i[t]}));return Object.keys(e).forEach((t=>{const s=new D(e[t].attr,i[t]);s.isValid?i[t]=s:A.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(e[t].attr)}"`)})),i}(t.dateRanges,e.dateRanges,e.recentlyRemovedDateranges));const a=e.fragments;if(n){A.warn("discontinuity sliding from playlist, take drift into account");for(let t=0;t{e.elementaryStreams=t.elementaryStreams,e.stats=t.stats})),r?rs(e,r,r.startPTS,r.endPTS,r.startDTS,r.endDTS):as(t,e),a.length&&(e.totalduration=e.edge-a[0].start),e.driftStartTime=t.driftStartTime,e.driftStart=t.driftStart;const o=e.advancedDateTime;if(e.advanced&&o){const t=e.edge;e.driftStart||(e.driftStartTime=o,e.driftStart=t),e.driftEndTime=o,e.driftEnd=t}else e.driftEndTime=t.driftEndTime,e.driftEnd=t.driftEnd,e.advancedDateTime=t.advancedDateTime}function as(t,e){const s=e.startSN+e.skippedSegments-t.startSN,i=t.fragments;s<0||s>=i.length||os(e,i[s].start)}function os(t,e){if(e){const s=t.fragments;for(let i=t.skippedSegments;i{const{details:s}=t;null!=s&&s.fragments&&s.fragments.forEach((t=>{t.level=e}))}))}function cs(t){switch(t.details){case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_TIMEOUT:case y.LEVEL_LOAD_TIMEOUT:case y.MANIFEST_LOAD_TIMEOUT:return!0}return!1}function us(t,e){const s=cs(e);return t.default[(s?"timeout":"error")+"Retry"]}function fs(t,e){const s="linear"===t.backoff?1:Math.pow(2,e);return Math.min(s*t.retryDelayMs,t.maxRetryDelayMs)}function gs(t){return h(h({},t),{errorRetry:null,timeoutRetry:null})}function ms(t,e,s,i){if(!t)return!1;const r=null==i?void 0:i.code,n=e499)}(r)||!!s);return t.shouldRetry?t.shouldRetry(t,e,s,i,n):n}const ps=function(t,e){let s=0,i=t.length-1,r=null,n=null;for(;s<=i;){r=(s+i)/2|0,n=t[r];const a=e(n);if(a>0)s=r+1;else{if(!(a<0))return n;i=r-1}}return null};function vs(t,e,s=0,i=0,r=.005){let n=null;if(t){n=e[t.sn-e[0].sn+1]||null;const i=t.endDTS-s;i>0&&i<15e-7&&(s+=15e-7)}else 0===s&&0===e[0].start&&(n=e[0]);if(n&&((!t||t.level===n.level)&&0===ys(s,i,n)||function(t,e,s){if(e&&0===e.start&&e.level0){const i=e.tagList.reduce(((t,e)=>("INF"===e[0]&&(t+=parseFloat(e[1])),t)),s);return t.start<=i}return!1}(n,t,Math.min(r,i))))return n;const a=ps(e,ys.bind(null,s,i));return!a||a===t&&n?n:a}function ys(t=0,e=0,s){if(s.start<=t&&s.start+s.duration>t)return 0;const i=Math.min(e,s.duration+(s.deltaPTS?s.deltaPTS:0));return s.start+s.duration-i<=t?1:s.start-i>t&&s.start?-1:0}function Es(t,e,s){const i=1e3*Math.min(e,s.duration+(s.deltaPTS?s.deltaPTS:0));return(s.endProgramDateTime||0)-i>t}var Ts=0,Ss=2,Ls=3,As=5,Rs=0,bs=1,ks=2;class ws{constructor(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=A.log.bind(A,`${e}:`),this.warn=A.warn.bind(A,`${e}:`),this.hls=t}destroy(){this.clearTimer(),this.hls=this.log=this.warn=null}clearTimer(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)}startLoad(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()}stopLoad(){this.canLoad=!1,this.clearTimer()}switchParams(t,e,s){const i=null==e?void 0:e.renditionReports;if(i){let r=-1;for(let s=0;s=0&&t>e.partTarget&&(a+=1)}const o=s&&Ze(s);return new ts(n,a>=0?a:void 0,o)}}}loadPlaylist(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())}shouldLoadPlaylist(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)}shouldReloadPlaylist(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)}playlistLoaded(t,e,s){const{details:i,stats:r}=e,n=self.performance.now(),a=r.loading.first?Math.max(0,n-r.loading.first):0;if(i.advancedDateTime=Date.now()-a,i.live||null!=s&&s.live){if(i.reloaded(s),s&&this.log(`live playlist ${t} ${i.advanced?"REFRESHED "+i.lastPartSn+"-"+i.lastPartIndex:i.updated?"UPDATED":"MISSED"}`),s&&i.fragments.length>0&&ns(s,i),!this.canLoad||!i.live)return;let a,o,l;if(i.canBlockReload&&i.endSN&&i.advanced){const t=this.hls.config.lowLatencyMode,r=i.lastPartSn,n=i.endSN,h=i.lastPartIndex,d=r===n;-1!==h?(o=d?n+1:r,l=d?t?0:h:h+1):o=n+1;const c=i.age,u=c+i.ageHeader;let f=Math.min(u-i.partTarget,1.5*i.targetduration);if(f>0){if(s&&f>s.tuneInGoal)this.warn(`CDN Tune-in goal increased from: ${s.tuneInGoal} to: ${f} with playlist age: ${i.age}`),f=0;else{const t=Math.floor(f/i.targetduration);if(o+=t,void 0!==l){l+=Math.round(f%i.targetduration/i.partTarget)}this.log(`CDN Tune-in age: ${i.ageHeader}s last advanced ${c.toFixed(2)}s goal: ${f} skip sn ${t} to part ${l}`)}i.tuneInGoal=f}if(a=this.getDeliveryDirectives(i,e.deliveryDirectives,o,l),t||!d)return void this.loadPlaylist(a)}else(i.canBlockReload||i.canSkipUntil)&&(a=this.getDeliveryDirectives(i,e.deliveryDirectives,o,l));const h=this.hls.mainForwardBufferInfo,d=h?h.end-h.len:0,c=function(t,e=1/0){let s=1e3*t.targetduration;if(t.updated){const i=t.fragments,r=4;if(i.length&&s*r>e){const t=1e3*i[i.length-1].duration;tthis.requestScheduled+c&&(this.requestScheduled=r.loading.start),void 0!==o&&i.canBlockReload?this.requestScheduled=r.loading.first+c-(1e3*i.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+cthis.loadPlaylist(a)),u)}else this.clearTimer()}getDeliveryDirectives(t,e,s,i){let r=Ze(t);return null!=e&&e.skip&&t.deltaUpdateFailed&&(s=e.msn,i=e.part,r=ze),new ts(s,i,r)}checkRetry(t){const e=t.details,s=cs(t),i=t.errorAction,{action:r,retryCount:n=0,retryConfig:a}=i||{},o=!!i&&!!a&&(r===As||!i.resolved&&r===Ss);if(o){var l;if(this.requestScheduled=-1,n>=a.maxNumRetry)return!1;if(s&&null!=(l=t.context)&&l.deliveryDirectives)this.warn(`Retrying playlist loading ${n+1}/${a.maxNumRetry} after "${e}" without delivery-directives`),this.loadPlaylist();else{const t=fs(a,n);this.timer=self.setTimeout((()=>this.loadPlaylist()),t),this.warn(`Retrying playlist loading ${n+1}/${a.maxNumRetry} after "${e}" in ${t}ms`)}t.levelRetry=!0,i.resolved=!0}return o}}class Ds{constructor(t,e=0,s=0){this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=s}sample(t,e){const s=Math.pow(this.alpha_,t);this.estimate_=e*(1-s)+s*this.estimate_,this.totalWeight_+=t}getTotalWeight(){return this.totalWeight_}getEstimate(){if(this.alpha_){const t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_}}class Is{constructor(t,e,s,i=100){this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=s,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Ds(t),this.fast_=new Ds(e),this.defaultTTFB_=i,this.ttfb_=new Ds(t)}update(t,e){const{slow_:s,fast_:i,ttfb_:r}=this;s.halfLife!==t&&(this.slow_=new Ds(t,s.getEstimate(),s.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Ds(e,i.getEstimate(),i.getTotalWeight())),r.halfLife!==t&&(this.ttfb_=new Ds(t,r.getEstimate(),r.getTotalWeight()))}sample(t,e){const s=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/s;this.fast_.sample(s,i),this.slow_.sample(s,i)}sampleTTFB(t){const e=t/1e3,s=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(s,Math.max(t,5))}canEstimate(){return this.fast_.getTotalWeight()>=this.minWeight_}getEstimate(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_}getEstimateTTFB(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_}destroy(){}}const _s={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},Cs={};function xs(t,e,s,i,r,n){const a=t.audioCodec?t.audioGroups:null,o=null==n?void 0:n.audioCodec,l=null==n?void 0:n.channels,h=l?parseInt(l):o?1/0:2;let d=null;if(null!=a&&a.length)try{d=1===a.length&&a[0]?e.groups[a[0]].channels:a.reduce(((t,s)=>{if(s){const i=e.groups[s];if(!i)throw new Error(`Audio track group ${s} not found`);Object.keys(i.channels).forEach((e=>{t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==s||t.bitrate>Math.max(r,8e6))||!!d&&f(h)&&Object.keys(d).some((t=>parseInt(t)>h))}function Ps(t,e,s){const i=t.videoCodec,r=t.audioCodec;if(!i||!r||!s)return Promise.resolve(_s);const n={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},a=t.videoRange;"SDR"!==a&&(n.transferFunction=a.toLowerCase());const o=i.split(",").map((t=>({type:"media-source",video:h(h({},n),{},{contentType:re(t,"video")})})));return r&&t.audioGroups&&t.audioGroups.forEach((t=>{var s;t&&(null==(s=e.groups[t])||s.tracks.forEach((e=>{if(e.groupId===t){const t=e.channels||"",s=parseFloat(t);f(s)&&s>2&&o.push.apply(o,r.split(",").map((t=>({type:"media-source",audio:{contentType:re(t,"audio"),channels:""+s}}))))}})))})),Promise.all(o.map((t=>{const e=function(t){const{audio:e,video:s}=t,i=s||e;if(i){const t=i.contentType.split('"')[1];if(s)return`r${s.height}x${s.width}f${Math.ceil(s.framerate)}${s.transferFunction||"sd"}_${t}_${Math.ceil(s.bitrate/1e5)}`;if(e)return`c${e.channels}${e.spatialRendering?"s":"n"}_${t}`}return""}(t);return Cs[e]||(Cs[e]=s.decodingInfo(t))}))).then((t=>({supported:!t.some((t=>!t.supported)),configurations:o,decodingInfoResults:t}))).catch((t=>({supported:!1,configurations:o,decodingInfoResults:[],error:t})))}function Ms(t,e){let s=!1,i=[];return t&&(s="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||Xe.slice(0),s=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){const t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}(),i=s?i.filter((t=>"SDR"!==t)):["SDR"]),{preferHDR:s,allowedVideoRanges:i}}function Fs(t,e){A.log(`[abr] start candidates with "${t}" ignored because ${e}`)}function Os(t,e,s){if("attrs"in t){const s=e.indexOf(t);if(-1!==s)return s}for(let i=0;i-1===i.indexOf(t)))}(o,e.characteristics))&&(void 0===s||s(t,e))}function Us(t,e){const{audioCodec:s,channels:i}=t;return!(void 0!==s&&(e.audioCodec||"").substring(0,4)!==s.substring(0,4)||void 0!==i&&i!==(e.channels||"2"))}function Bs(t,e,s){for(let i=e;i;i--)if(s(t[i]))return i;for(let i=e+1;i1&&this.tickImmediate(),this._tickCallCount=0)}tickImmediate(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)}doTick(){}}var Gs="NOT_LOADED",Ks="APPENDING",Hs="PARTIAL",Vs="OK";class Ys{constructor(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}_registerListeners(){const{hls:t}=this;t.on(p.BUFFER_APPENDED,this.onBufferAppended,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.BUFFER_APPENDED,this.onBufferAppended,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this)}destroy(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null}getAppendedFrag(t,e){const s=this.activePartLists[e];if(s)for(let e=s.length;e--;){const i=s[e];if(!i)break;const r=i.end;if(i.start<=t&&null!==r&&t<=r)return i}return this.getBufferedFrag(t,e)}getBufferedFrag(t,e){const{fragments:s}=this,i=Object.keys(s);for(let r=i.length;r--;){const n=s[i[r]];if((null==n?void 0:n.body.type)===e&&n.buffered){const e=n.body;if(e.start<=t&&t<=e.end)return e}}return null}detectEvictedFragments(t,e,s,i){this.timeRanges&&(this.timeRanges[t]=e);const r=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((i=>{const n=this.fragments[i];if(!n)return;if(r>=n.body.sn)return;if(!n.buffered&&!n.loaded)return void(n.body.type===s&&this.removeFragment(n.body));const a=n.range[t];a&&a.time.some((t=>{const s=!this.isTimeBuffered(t.startPTS,t.endPTS,e);return s&&this.removeFragment(n.body),s}))}))}detectPartialFragments(t){const e=this.timeRanges,{frag:s,part:i}=t;if(!e||"initSegment"===s.sn)return;const r=js(s),n=this.fragments[r];if(!n||n.buffered&&s.gap)return;const a=!s.relurl;if(Object.keys(e).forEach((t=>{const r=s.elementaryStreams[t];if(!r)return;const o=e[t],l=a||!0===r.partial;n.range[t]=this.getBufferedTimes(s,i,l,o)})),n.loaded=null,Object.keys(n.range).length){n.buffered=!0;(n.body.endList=s.endList||n.body.endList)&&(this.endListFragments[n.body.type]=n),Ws(n)||this.removeParts(s.sn-1,s.type)}else this.removeFragment(n.body)}removeParts(t,e){const s=this.activePartLists[e];s&&(this.activePartLists[e]=s.filter((e=>e.fragment.sn>=t)))}fragBuffered(t,e){const s=js(t);let i=this.fragments[s];!i&&e&&(i=this.fragments[s]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)}getBufferedTimes(t,e,s,i){const r={time:[],partial:s},n=t.start,a=t.end,o=t.minEndPTS||a,l=t.maxStartPTS||n;for(let t=0;t=e&&o<=s){r.time.push({startPTS:Math.max(n,i.start(t)),endPTS:Math.min(a,i.end(t))});break}if(ne){const e=Math.max(n,i.start(t)),s=Math.min(a,i.end(t));s>e&&(r.partial=!0,r.time.push({startPTS:e,endPTS:s}))}else if(a<=e)break}return r}getPartialFragment(t){let e,s,i,r=null,n=0;const{bufferPadding:a,fragments:o}=this;return Object.keys(o).forEach((l=>{const h=o[l];h&&Ws(h)&&(s=h.body.start-a,i=h.body.end+a,t>=s&&t<=i&&(e=Math.min(t-s,i-t),n<=e&&(r=h.body,n=e)))})),r}isEndListAppended(t){const e=this.endListFragments[t];return void 0!==e&&(e.buffered||Ws(e))}getState(t){const e=js(t),s=this.fragments[e];return s?s.buffered?Ws(s)?Hs:Vs:Ks:Gs}isTimeBuffered(t,e,s){let i,r;for(let n=0;n=i&&e<=r)return!0;if(e<=i)return!1}return!1}onFragLoaded(t,e){const{frag:s,part:i}=e;if("initSegment"===s.sn||s.bitrateTest)return;const r=i?null:e,n=js(s);this.fragments[n]={body:s,appendedPTS:null,loaded:r,buffered:!1,range:Object.create(null)}}onBufferAppended(t,e){const{frag:s,part:i,timeRanges:r}=e;if("initSegment"===s.sn)return;const n=s.type;if(i){let t=this.activePartLists[n];t||(this.activePartLists[n]=t=[]),t.push(i)}this.timeRanges=r,Object.keys(r).forEach((t=>{const e=r[t];this.detectEvictedFragments(t,e,n,i)}))}onFragBuffered(t,e){this.detectPartialFragments(e)}hasFragment(t){const e=js(t);return!!this.fragments[e]}hasParts(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)}removeFragmentsInRange(t,e,s,i,r){i&&!this.hasGaps||Object.keys(this.fragments).forEach((n=>{const a=this.fragments[n];if(!a)return;const o=a.body;o.type!==s||i&&!o.gap||o.startt&&(a.buffered||r)&&this.removeFragment(o)}))}removeFragment(t){const e=js(t);t.stats.loaded=0,t.clearElementaryStreamInfo();const s=this.activePartLists[t.type];if(s){const e=t.sn;this.activePartLists[t.type]=s.filter((t=>t.fragment.sn!==e))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]}removeAllFragments(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1}}function Ws(t){var e,s,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(s=t.range.audio)?void 0:s.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function js(t){return`${t.type}_${t.level}_${t.sn}`}const qs={length:0,start:()=>0,end:()=>0};class Xs{static isBuffered(t,e){try{if(t){const s=Xs.getBuffered(t);for(let t=0;t=s.start(t)&&e<=s.end(t))return!0}}catch(t){}return!1}static bufferInfo(t,e,s){try{if(t){const i=Xs.getBuffered(t),r=[];let n;for(n=0;nn&&(i[r-1].end=t[e].end):i.push(t[e])}else i.push(t[e])}else i=t;let r,n=0,a=e,o=e;for(let t=0;t=l&&es.startCC||t&&t.cc{if(this.loader&&this.loader.destroy(),t.gap){if(t.tagList.some((t=>"GAP"===t[0])))return void o(ni(t));t.gap=!1}const l=this.loader=t.loader=r?new r(i):new n(i),d=ri(t),c=gs(i.fragLoadPolicy.default),u={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:"initSegment"===t.sn?1/0:si};t.stats=l.stats,l.load(d,u,{onSuccess:(e,s,i,r)=>{this.resetLoader(t,l);let n=e.data;i.resetIV&&t.decryptdata&&(t.decryptdata.iv=new Uint8Array(n.slice(0,16)),n=n.slice(16)),a({frag:t,part:null,payload:n,networkDetails:r})},onError:(e,i,r,n)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_ERROR,fatal:!1,frag:t,response:h({url:s,data:void 0},e),error:new Error(`HTTP Error ${e.code} ${e.text}`),networkDetails:r,stats:n}))},onAbort:(e,s,i)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.INTERNAL_ABORTED,fatal:!1,frag:t,error:new Error("Aborted"),networkDetails:i,stats:e}))},onTimeout:(e,s,i)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,error:new Error(`Timeout after ${u.timeout}ms`),networkDetails:i,stats:e}))},onProgress:(s,i,r,n)=>{e&&e({frag:t,part:null,payload:r,networkDetails:n})}})}))}loadPart(t,e,s){this.abort();const i=this.config,r=i.fLoader,n=i.loader;return new Promise(((a,o)=>{if(this.loader&&this.loader.destroy(),t.gap||e.gap)return void o(ni(t,e));const l=this.loader=t.loader=r?new r(i):new n(i),d=ri(t,e),c=gs(i.fragLoadPolicy.default),u={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:si};e.stats=l.stats,l.load(d,u,{onSuccess:(i,r,n,o)=>{this.resetLoader(t,l),this.updateStatsFromPart(t,e);const h={frag:t,part:e,payload:i.data,networkDetails:o};s(h),a(h)},onError:(s,i,r,n)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_ERROR,fatal:!1,frag:t,part:e,response:h({url:d.url,data:void 0},s),error:new Error(`HTTP Error ${s.code} ${s.text}`),networkDetails:r,stats:n}))},onAbort:(s,i,r)=>{t.stats.aborted=e.stats.aborted,this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.INTERNAL_ABORTED,fatal:!1,frag:t,part:e,error:new Error("Aborted"),networkDetails:r,stats:s}))},onTimeout:(s,i,r)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,part:e,error:new Error(`Timeout after ${u.timeout}ms`),networkDetails:r,stats:s}))}})}))}updateStatsFromPart(t,e){const s=t.stats,i=e.stats,r=i.total;if(s.loaded+=i.loaded,r){const i=Math.round(t.duration/e.duration),n=Math.min(Math.round(s.loaded/r),i),a=(i-n)*Math.round(s.loaded/n);s.total=s.loaded+a}else s.total=Math.max(s.loaded,s.total);const n=s.loading,a=i.loading;n.start?n.first+=a.first-a.start:(n.start=a.start,n.first=a.first),n.end=a.end}resetLoader(t,e){t.loader=null,this.loader===e&&(self.clearTimeout(this.partLoadTimeout),this.loader=null),e.destroy()}}function ri(t,e=null){const s=e||t,i={frag:t,part:e,responseType:"arraybuffer",url:s.url,headers:{},rangeStart:0,rangeEnd:0},r=s.byteRangeStartOffset,n=s.byteRangeEndOffset;if(f(r)&&f(n)){var a;let e=r,s=n;if("initSegment"===t.sn&&"AES-128"===(null==(a=t.decryptdata)?void 0:a.method)){const t=n-r;t%16&&(s=n+(16-t%16)),0!==r&&(i.resetIV=!0,e=r-16)}i.rangeStart=e,i.rangeEnd=s}return i}function ni(t,e){const s=new Error(`GAP ${t.gap?"tag":"attribute"} found`),i={type:v.MEDIA_ERROR,details:y.FRAG_GAP,fatal:!1,frag:t,error:s,networkDetails:null};return e&&(i.part=e),(e||t).stats.aborted=!0,new ai(i)}class ai extends Error{constructor(t){super(t.error.message),this.data=void 0,this.data=t}}class oi{constructor(t,e){this.subtle=void 0,this.aesIV=void 0,this.subtle=t,this.aesIV=e}decrypt(t,e){return this.subtle.decrypt({name:"AES-CBC",iv:this.aesIV},e,t)}}class li{constructor(t,e){this.subtle=void 0,this.key=void 0,this.subtle=t,this.key=e}expandKey(){return this.subtle.importKey("raw",this.key,{name:"AES-CBC"},!1,["encrypt","decrypt"])}}class hi{constructor(){this.rcon=[0,1,2,4,8,16,32,64,128,27,54],this.subMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.invSubMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.sBox=new Uint32Array(256),this.invSBox=new Uint32Array(256),this.key=new Uint32Array(0),this.ksRows=0,this.keySize=0,this.keySchedule=void 0,this.invKeySchedule=void 0,this.initTable()}uint8ArrayToUint32Array_(t){const e=new DataView(t),s=new Uint32Array(4);for(let t=0;t<4;t++)s[t]=e.getUint32(4*t);return s}initTable(){const t=this.sBox,e=this.invSBox,s=this.subMix,i=s[0],r=s[1],n=s[2],a=s[3],o=this.invSubMix,l=o[0],h=o[1],d=o[2],c=o[3],u=new Uint32Array(256);let f=0,g=0,m=0;for(m=0;m<256;m++)u[m]=m<128?m<<1:m<<1^283;for(m=0;m<256;m++){let s=g^g<<1^g<<2^g<<3^g<<4;s=s>>>8^255&s^99,t[f]=s,e[s]=f;const o=u[f],m=u[o],p=u[m];let v=257*u[s]^16843008*s;i[f]=v<<24|v>>>8,r[f]=v<<16|v>>>16,n[f]=v<<8|v>>>24,a[f]=v,v=16843009*p^65537*m^257*o^16843008*f,l[s]=v<<24|v>>>8,h[s]=v<<16|v>>>16,d[s]=v<<8|v>>>24,c[s]=v,f?(f=o^u[u[u[p^o]]],g^=u[u[g]]):f=g=1}}expandKey(t){const e=this.uint8ArrayToUint32Array_(t);let s=!0,i=0;for(;i{if(!this.subtle)return Promise.reject(new Error("web crypto not initialized"));this.logOnce("WebCrypto AES decrypt");return new oi(this.subtle,new Uint8Array(s)).decrypt(t.buffer,e)})).catch((i=>(A.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${i.name}: ${i.message}`),this.onWebCryptoError(t,e,s))))}onWebCryptoError(t,e,s){this.useSoftware=!0,this.logEnabled=!0,this.softwareDecrypt(t,e,s);const i=this.flush();if(i)return i.buffer;throw new Error("WebCrypto and softwareDecrypt: failed to decrypt data")}getValidChunk(t){let e=t;const s=t.length-t.length%16;return s!==t.length&&(e=st(t,0,s),this.remainderData=st(t,s)),e}logOnce(t){this.logEnabled&&(A.log(`[decrypter]: ${t}`),this.logEnabled=!1)}}const ci=function(t){let e="";const s=t.length;for(let i=0;ia.end){const t=n>r;(n{if(this.fragContextChanged(t))return this.warn(`Fragment ${t.sn}${e.part?" p: "+e.part.index:""} of level ${t.level} was dropped during download.`),void this.fragmentTracker.removeFragment(t);t.stats.chunkCount++,this._handleFragmentLoadProgress(e)})).then((e=>{if(!e)return;const s=this.state;this.fragContextChanged(t)?(s===mi||!this.fragCurrent&&s===yi)&&(this.fragmentTracker.removeFragment(t),this.state=fi):("payload"in e&&(this.log(`Loaded fragment ${t.sn} of level ${t.level}`),this.hls.trigger(p.FRAG_LOADED,e)),this._handleFragmentLoadComplete(e))})).catch((e=>{this.state!==ui&&this.state!==Si&&(this.warn(`Frag error: ${(null==e?void 0:e.message)||e}`),this.resetFragmentLoading(t))}))}clearTrackerIfNeeded(t){var e;const{fragmentTracker:s}=this;if(s.getState(t)===Ks){const e=t.type,i=this.getFwdBufferInfo(this.mediaBuffer,e),r=Math.max(t.duration,i?i.len:this.config.maxBufferLength),n=this.backtrackFragment;(1===(n?t.sn-n.sn:0)||this.reduceMaxBufferLength(r,t.duration))&&s.removeFragment(t)}else 0===(null==(e=this.mediaBuffer)?void 0:e.buffered.length)?s.removeAllFragments():s.hasParts(t.type)&&(s.detectPartialFragments({frag:t,part:null,stats:t.stats,id:t.type}),s.getState(t)===Hs&&s.removeFragment(t))}checkLiveUpdate(t){if(t.updated&&!t.live){const e=t.fragments[t.fragments.length-1];this.fragmentTracker.detectPartialFragments({frag:e,part:null,stats:e.stats,id:e.type})}t.fragments[0]||(t.deltaUpdateFailed=!0)}flushMainBuffer(t,e,s=null){if(!(t-e))return;const i={startOffset:t,endOffset:e,type:s};this.hls.trigger(p.BUFFER_FLUSHING,i)}_loadInitSegment(t,e){this._doFragLoad(t,e).then((e=>{if(!e||this.fragContextChanged(t)||!this.levels)throw new Error("init load aborted");return e})).then((e=>{const{hls:s}=this,{payload:i}=e,r=t.decryptdata;if(i&&i.byteLength>0&&null!=r&&r.key&&r.iv&&"AES-128"===r.method){const n=self.performance.now();return this.decrypter.decrypt(new Uint8Array(i),r.key.buffer,r.iv.buffer).catch((e=>{throw s.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((i=>{const r=self.performance.now();return s.trigger(p.FRAG_DECRYPTED,{frag:t,payload:i,stats:{tstart:n,tdecrypt:r}}),e.payload=i,this.completeInitSegmentLoad(e)}))}return this.completeInitSegmentLoad(e)})).catch((e=>{this.state!==ui&&this.state!==Si&&(this.warn(e),this.resetFragmentLoading(t))}))}completeInitSegmentLoad(t){const{levels:e}=this;if(!e)throw new Error("init load aborted, missing levels");const s=t.frag.stats;this.state=fi,t.frag.data=new Uint8Array(t.payload),s.parsing.start=s.buffering.start=self.performance.now(),s.parsing.end=s.buffering.end=self.performance.now(),this.tick()}fragContextChanged(t){const{fragCurrent:e}=this;return!t||!e||t.sn!==e.sn||t.level!==e.level}fragBufferedComplete(t,e){var s,i,r,n;const a=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log(`Buffered ${t.type} sn: ${t.sn}${e?" part: "+e.index:""} of ${this.playlistType===De?"level":"track"} ${t.level} (frag:[${(null!=(s=t.startPTS)?s:NaN).toFixed(3)}-${(null!=(i=t.endPTS)?i:NaN).toFixed(3)}] > buffer:${a?ci(Xs.getBuffered(a)):"(detached)"})`),"initSegment"!==t.sn){var o;if(t.type!==_e){const e=t.elementaryStreams;if(!Object.keys(e).some((t=>!!e[t])))return void(this.state=fi)}const e=null==(o=this.levels)?void 0:o[t.level];null!=e&&e.fragmentError&&(this.log(`Resetting level fragment error count of ${e.fragmentError} on frag buffered`),e.fragmentError=0)}this.state=fi,a&&(!this.loadedmetadata&&t.type==De&&a.buffered.length&&(null==(r=this.fragCurrent)?void 0:r.sn)===(null==(n=this.fragPrevious)?void 0:n.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())}seekToStartPos(){}_handleFragmentLoadComplete(t){const{transmuxer:e}=this;if(!e)return;const{frag:s,part:i,partsLoaded:r}=t,n=!r||0===r.length||r.some((t=>!t)),a=new zs(s.level,s.sn,s.stats.chunkCount+1,0,i?i.index:-1,!n);e.flush(a)}_handleFragmentLoadProgress(t){}_doFragLoad(t,e,s=null,i){var r;const n=null==e?void 0:e.details;if(!this.levels||!n)throw new Error(`frag load aborted, missing level${n?"":" detail"}s`);let a=null;if(!t.encrypted||null!=(r=t.decryptdata)&&r.key?!t.encrypted&&n.encryptedFragments.length&&this.keyLoader.loadClear(t,n.encryptedFragments):(this.log(`Loading key for ${t.sn} of [${n.startSN}-${n.endSN}], ${"[stream-controller]"===this.logPrefix?"level":"track"} ${t.level}`),this.state=gi,this.fragCurrent=t,a=this.keyLoader.load(t).then((t=>{if(!this.fragContextChanged(t.frag))return this.hls.trigger(p.KEY_LOADED,t),this.state===gi&&(this.state=fi),t})),this.hls.trigger(p.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(a=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),s=Math.max(t.start,s||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){const r=n.partList;if(r&&i){s>t.end&&n.fragmentHint&&(t=n.fragmentHint);const o=this.getNextPart(r,t,s);if(o>-1){const l=r[o];let h;return this.log(`Loading part sn: ${t.sn} p: ${l.index} cc: ${t.cc} of playlist [${n.startSN}-${n.endSN}] parts [0-${o}-${r.length-1}] ${"[stream-controller]"===this.logPrefix?"level":"track"}: ${t.level}, target: ${parseFloat(s.toFixed(3))}`),this.nextLoadPosition=l.start+l.duration,this.state=mi,h=a?a.then((s=>!s||this.fragContextChanged(s.frag)?null:this.doFragPartsLoad(t,l,e,i))).catch((t=>this.handleFragLoadError(t))):this.doFragPartsLoad(t,l,e,i).catch((t=>this.handleFragLoadError(t))),this.hls.trigger(p.FRAG_LOADING,{frag:t,part:l,targetBufferTime:s}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(r,s))return Promise.resolve(null)}}this.log(`Loading fragment ${t.sn} cc: ${t.cc} ${n?"of ["+n.startSN+"-"+n.endSN+"] ":""}${"[stream-controller]"===this.logPrefix?"level":"track"}: ${t.level}, target: ${parseFloat(s.toFixed(3))}`),f(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=mi;const o=this.config.progressive;let l;return l=o&&a?a.then((e=>!e||this.fragContextChanged(null==e?void 0:e.frag)?null:this.fragmentLoader.load(t,i))).catch((t=>this.handleFragLoadError(t))):Promise.all([this.fragmentLoader.load(t,o?i:void 0),a]).then((([t])=>(!o&&t&&i&&i(t),t))).catch((t=>this.handleFragLoadError(t))),this.hls.trigger(p.FRAG_LOADING,{frag:t,targetBufferTime:s}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):l}doFragPartsLoad(t,e,s,i){return new Promise(((r,n)=>{var a;const o=[],l=null==(a=s.details)?void 0:a.partList,h=e=>{this.fragmentLoader.loadPart(t,e,i).then((i=>{o[e.index]=i;const n=i.part;this.hls.trigger(p.FRAG_LOADED,i);const a=ls(s,t.sn,e.index+1)||hs(l,t.sn,e.index+1);if(!a)return r({frag:t,part:n,partsLoaded:o});h(a)})).catch(n)};h(e)}))}handleFragLoadError(t){if("data"in t){const e=t.data;t.data&&e.details===y.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(p.ERROR,e)}else this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null}_handleTransmuxerFlush(t){const e=this.getCurrentContext(t);if(!e||this.state!==yi)return void(this.fragCurrent||this.state===ui||this.state===Si||(this.state=fi));const{frag:s,part:i,level:r}=e,n=self.performance.now();s.stats.parsing.end=n,i&&(i.stats.parsing.end=n),this.updateLevelTiming(s,i,r,t.partial)}getCurrentContext(t){const{levels:e,fragCurrent:s}=this,{level:i,sn:r,part:n}=t;if(null==e||!e[i])return this.warn(`Levels object was unset while buffering fragment ${r} of level ${i}. The current chunk will not be buffered.`),null;const a=e[i],o=n>-1?ls(a,r,n):null,l=o?o.fragment:function(t,e,s){if(null==t||!t.details)return null;const i=t.details;let r=i.fragments[e-i.startSN];return r||(r=i.fragmentHint,r&&r.sn===e?r:en&&this.flushMainBuffer(a,t.start)}getFwdBufferInfo(t,e){const s=this.getLoadPosition();return f(s)?this.getFwdBufferInfoAtPos(t,s,e):null}getFwdBufferInfoAtPos(t,e,s){const{config:{maxBufferHole:i}}=this,r=Xs.bufferInfo(t,e,i);if(0===r.len&&void 0!==r.nextStart){const n=this.fragmentTracker.getBufferedFrag(e,s);if(n&&r.nextStart=i&&(s.maxMaxBufferLength=r,this.warn(`Reduce max buffer length to ${r}s`),!0)}getAppendedFrag(t,e=De){const s=this.fragmentTracker.getAppendedFrag(t,De);return s&&"fragment"in s?s.fragment:s}getNextFragment(t,e){const s=e.fragments,i=s.length;if(!i)return null;const{config:r}=this,n=s[0].start;let a;if(e.live){const o=r.initialLiveManifestSize;if(ie}getNextFragmentLoopLoading(t,e,s,i,r){const n=t.gap,a=this.getNextFragment(this.nextLoadPosition,e);if(null===a)return a;if(t=a,n&&t&&!t.gap&&s.nextStart){const e=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,s.nextStart,i);if(null!==e&&s.len+e.len>=r)return this.log(`buffer full after gaps in "${i}" playlist starting at sn: ${t.sn}`),null}return t}mapToInitFragWhenRequired(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment}getNextPart(t,e,s){let i=-1,r=!1,n=!0;for(let a=0,o=t.length;a-1&&ss.start&&s.loaded}getInitialLiveFragment(t,e){const s=this.fragPrevious;let i=null;if(s){if(t.hasProgramDateTime&&(this.log(`Live playlist, switching playlist, load frag with same PDT: ${s.programDateTime}`),i=function(t,e,s){if(null===e||!Array.isArray(t)||!t.length||!f(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;s=s||0;for(let i=0;i=t.startSN&&r<=t.endSN){const n=e[r-t.startSN];s.cc===n.cc&&(i=n,this.log(`Live playlist, switching playlist, load frag with next SN: ${i.sn}`))}i||(i=function(t,e){return ps(t,(t=>t.cce?-1:0))}(e,s.cc),i&&this.log(`Live playlist, switching playlist, load frag with same CC: ${i.sn}`))}}else{const e=this.hls.liveSyncPosition;null!==e&&(i=this.getFragmentAtPosition(e,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i}getFragmentAtPosition(t,e,s){const{config:i}=this;let{fragPrevious:r}=this,{fragments:n,endSN:a}=s;const{fragmentHint:o}=s,{maxFragLookUpTolerance:l}=i,h=s.partList,d=!!(i.lowLatencyMode&&null!=h&&h.length&&o);let c;if(d&&o&&!this.bitrateTest&&(n=n.concat(o),a=o.sn),te-l?0:l)}else c=n[n.length-1];if(c){const t=c.sn-s.startSN,e=this.fragmentTracker.getState(c);if((e===Vs||e===Hs&&c.gap)&&(r=c),r&&c.sn===r.sn&&(!d||h[0].fragment.sn>c.sn)){if(r&&c.level===r.level){const e=n[t+1];c=c.sn=n-e.maxFragLookUpTolerance&&r<=a;if(null!==i&&s.duration>i&&(r${t.startSN} prev-sn: ${r?r.sn:"na"} fragments: ${i}`),n}return r}waitForCdnTuneIn(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)}setStartPosition(t,e){let s=this.startPosition;if(s ${null==(i=this.fragCurrent)?void 0:i.url}`);const r=e.details===y.FRAG_GAP;r&&this.fragmentTracker.fragBuffered(s,!0);const n=e.errorAction,{action:a,retryCount:o=0,retryConfig:l}=n||{};if(n&&a===As&&l){this.resetStartWhenNotLoaded(this.levelLastLoaded);const i=fs(l,o);this.warn(`Fragment ${s.sn} of ${t} ${s.level} errored with ${e.details}, retrying loading ${o+1}/${l.maxNumRetry} in ${i}ms`),n.resolved=!0,this.retryDate=self.performance.now()+i,this.state=pi}else if(l&&n){if(this.resetFragmentErrors(t),!(o.5;r&&this.reduceMaxBufferLength(i.len,(null==e?void 0:e.duration)||10);const n=!r;return n&&this.warn(`Buffer full error while media.currentTime is not buffered, flush ${s} buffer`),e&&(this.fragmentTracker.removeFragment(e),this.nextLoadPosition=e.start),this.resetLoadingState(),n}return!1}resetFragmentErrors(t){t===Ie&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==ui&&(this.state=fi)}afterBufferFlushed(t,e,s){if(!t)return;const i=Xs.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,s),this.state===Ti&&this.resetLoadingState()}resetLoadingState(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=fi}resetStartWhenNotLoaded(t){if(!this.loadedmetadata){this.startFragRequested=!1;const e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}}resetWhenMissingContext(t){this.warn(`The loading context changed while buffering fragment ${t.sn} of level ${t.level}. This chunk will not be buffered.`),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()}removeUnbufferedFrags(t=0){this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)}updateLevelTiming(t,e,s,i){var r;const n=s.details;if(!n)return void this.warn("level.details undefined");if(!Object.keys(t.elementaryStreams).reduce(((e,r)=>{const a=t.elementaryStreams[r];if(a){const o=a.endPTS-a.startPTS;if(o<=0)return this.warn(`Could not parse fragment ${t.sn} ${r} duration reliably (${o})`),e||!1;const l=i?0:rs(n,t,a.startPTS,a.endPTS,a.startDTS,a.endDTS);return this.hls.trigger(p.LEVEL_PTS_UPDATED,{details:n,level:s,drift:l,type:r,frag:t,start:a.startPTS,end:a.endPTS}),!0}return e}),!1)&&null===(null==(r=this.transmuxer)?void 0:r.error)){const e=new Error(`Found no media in fragment ${t.sn} of level ${t.level} resetting transmuxer to fallback to playlist timing`);if(0===s.fragmentError&&(s.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(e.message),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,error:e,frag:t,reason:`Found no media in msn ${t.sn} of level "${s.url}"`}),!this.hls)return;this.resetTransmuxer()}this.state=Ei,this.hls.trigger(p.FRAG_PARSED,{frag:t,part:e})}resetTransmuxer(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)}recoverWorkerError(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())}set state(t){const e=this._state;e!==t&&(this._state=t,this.log(`${e}->${t}`))}get state(){return this._state}}class bi{constructor(){this.chunks=[],this.dataLength=0}push(t){this.chunks.push(t),this.dataLength+=t.length}flush(){const{chunks:t,dataLength:e}=this;let s;return t.length?(s=1===t.length?t[0]:function(t,e){const s=new Uint8Array(e);let i=0;for(let e=0;e0&&a.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Be,duration:Number.POSITIVE_INFINITY});r{if(f(t))return 90*t;return 9e4*e+(s?9e4*s.baseTime/s.timescale:0)};function Ii(t,e){return 255===t[e]&&240==(246&t[e+1])}function _i(t,e){return 1&t[e+1]?7:9}function Ci(t,e){return(3&t[e+3])<<11|t[e+4]<<3|(224&t[e+5])>>>5}function xi(t,e){return e+1=t.length)return!1;const i=Ci(t,e);if(i<=s)return!1;const r=e+i;return r===t.length||xi(t,r)}return!1}function Mi(t,e,s,i,r){if(!t.samplerate){const n=function(t,e,s,i){let r,n,a,o;const l=navigator.userAgent.toLowerCase(),h=i,d=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];r=1+((192&e[s+2])>>>6);const c=(60&e[s+2])>>>2;if(!(c>d.length-1))return a=(1&e[s+2])<<2,a|=(192&e[s+3])>>>6,A.log(`manifest codec:${i}, ADTS type:${r}, samplingIndex:${c}`),/firefox/i.test(l)?c>=6?(r=5,o=new Array(4),n=c-3):(r=2,o=new Array(2),n=c):-1!==l.indexOf("android")?(r=2,o=new Array(2),n=c):(r=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&c>=6?n=c-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(c>=6&&1===a||/vivaldi/i.test(l))||!i&&1===a)&&(r=2,o=new Array(2)),n=c)),o[0]=r<<3,o[0]|=(14&c)>>1,o[1]|=(1&c)<<7,o[1]|=a<<3,5===r&&(o[1]|=(14&n)>>1,o[2]=(1&n)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:d[c],channelCount:a,codec:"mp4a.40."+r,manifestCodec:h};{const e=new Error(`invalid ADTS sampling index:${c}`);t.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!0,error:e,reason:e.message})}}(e,s,i,r);if(!n)return;t.config=n.config,t.samplerate=n.samplerate,t.channelCount=n.channelCount,t.codec=n.codec,t.manifestCodec=n.manifestCodec,A.log(`parsed codec:${t.codec}, rate:${n.samplerate}, channels:${n.channelCount}`)}}function Fi(t){return 9216e4/t}function Oi(t,e,s,i,r){const n=i+r*Fi(t.samplerate),a=function(t,e){const s=_i(t,e);if(e+s<=t.length){const i=Ci(t,e)-s;if(i>0)return{headerLength:s,frameLength:i}}}(e,s);let o;if(a){const{frameLength:i,headerLength:r}=a,l=r+i,h=Math.max(0,s+l-e.length);h?(o=new Uint8Array(l-r),o.set(e.subarray(s+r,e.length),0)):o=e.subarray(s+r,s+l);const d={unit:o,pts:n};return h||t.samples.push(d),{sample:d,length:l,missing:h}}const l=e.length-s;o=new Uint8Array(l),o.set(e.subarray(s,e.length),0);return{sample:{unit:o,pts:n},length:l,missing:-1}}let Ni=null;const Ui=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],Bi=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],$i=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Gi=[0,1,1,4];function Ki(t,e,s,i,r){if(s+24>e.length)return;const n=Hi(e,s);if(n&&s+n.frameLength<=e.length){const a=i+r*(9e4*n.samplesPerFrame/n.sampleRate),o={unit:e.subarray(s,s+n.frameLength),pts:a,dts:a};return t.config=[],t.channelCount=n.channelCount,t.samplerate=n.sampleRate,t.samples.push(o),{sample:o,length:n.frameLength,missing:0}}}function Hi(t,e){const s=t[e+1]>>3&3,i=t[e+1]>>1&3,r=t[e+2]>>4&15,n=t[e+2]>>2&3;if(1!==s&&0!==r&&15!==r&&3!==n){const a=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Ui[14*(3===s?3-i:3===i?3:4)+r-1],h=Bi[3*(3===s?0:2===s?1:2)+n],d=3===o?1:2,c=$i[s][i],u=Gi[i],f=8*c*u,g=Math.floor(c*l/h+a)*u;if(null===Ni){const t=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Ni=t?parseInt(t[1]):0}return!!Ni&&Ni<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:h,channelCount:d,frameLength:g,samplesPerFrame:f}}}function Vi(t,e){return!(255!==t[e]||224&~t[e+1]||!(6&t[e+1]))}function Yi(t,e){return e+1{let s=0,i=5;e+=i;const r=new Uint32Array(1),n=new Uint32Array(1),a=new Uint8Array(1);for(;i>0;){a[0]=t[e];const o=Math.min(i,8),l=8-o;n[0]=4278190080>>>24+l<>l,s=s?s<e.length)return-1;if(11!==e[s]||119!==e[s+1])return-1;const n=e[s+4]>>6;if(n>=3)return-1;const a=[48e3,44100,32e3][n],o=63&e[s+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+n];if(s+l>e.length)return-1;const h=e[s+6]>>5;let d=0;2===h?d+=2:(1&h&&1!==h&&(d+=2),4&h&&(d+=2));const c=(e[s+6]<<8|e[s+7])>>12-d&1,u=[2,1,2,3,3,4,4,5][h]+c,f=e[s+5]>>3,g=7&e[s+5],m=new Uint8Array([n<<6|f<<1|g>>2,(3&g)<<6|h<<3|c<<2|o>>4,o<<4&224]),p=i+r*(1536/a*9e4),v=e.subarray(s,s+l);return t.config=m,t.channelCount=u,t.samplerate=a,t.samples.push({unit:v,pts:p}),l}class Qi{constructor(){this.VideoSample=null}createVideoSample(t,e,s,i){return{key:t,frame:!1,pts:e,dts:s,units:[],debug:i,length:0}}getLastNalUnit(t){var e;let s,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){const t=i.units;s=t[t.length-1]}return s}pushAccessUnit(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){const s=e.samples,i=s.length;if(!i)return void e.dropped++;{const e=s[i-1];t.pts=e.pts,t.dts=e.dts}}e.samples.push(t)}t.debug.length&&A.log(t.pts+"/"+t.dts+":"+t.debug)}}class Ji{constructor(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}loadWord(){const t=this.data,e=this.bytesAvailable,s=t.byteLength-e,i=new Uint8Array(4),r=Math.min(4,e);if(0===r)throw new Error("no bytes available");i.set(t.subarray(s,s+r)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*r,this.bytesAvailable-=r}skipBits(t){let e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(e=(t-=this.bitsAvailable)>>3,t-=e<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)}readBits(t){let e=Math.min(this.bitsAvailable,t);const s=this.word>>>32-e;if(t>32&&A.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return e=t-e,e>0&&this.bitsAvailable?s<>>t)return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()}skipUEG(){this.skipBits(1+this.skipLZ())}skipEG(){this.skipBits(1+this.skipLZ())}readUEG(){const t=this.skipLZ();return this.readBits(t+1)-1}readEG(){const t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)}readBoolean(){return 1===this.readBits(1)}readUByte(){return this.readBits(8)}readUShort(){return this.readBits(16)}readUInt(){return this.readBits(32)}skipScalingList(t){let e,s=8,i=8;for(let r=0;r{var n;switch(i.type){case 1:{let e=!1;a=!0;const r=i.data;if(l&&r.length>4){const t=new Ji(r).readSliceType();2!==t&&4!==t&&7!==t&&9!==t||(e=!0)}var h;if(e)null!=(h=o)&&h.frame&&!o.key&&(this.pushAccessUnit(o,t),o=this.VideoSample=null);o||(o=this.VideoSample=this.createVideoSample(!0,s.pts,s.dts,"")),o.frame=!0,o.key=e;break}case 5:a=!0,null!=(n=o)&&n.frame&&!o.key&&(this.pushAccessUnit(o,t),o=this.VideoSample=null),o||(o=this.VideoSample=this.createVideoSample(!0,s.pts,s.dts,"")),o.key=!0,o.frame=!0;break;case 6:a=!0,Kt(i.data,1,s.pts,e.samples);break;case 7:{var d,c;a=!0,l=!0;const e=i.data,s=new Ji(e).readSPS();if(!t.sps||t.width!==s.width||t.height!==s.height||(null==(d=t.pixelRatio)?void 0:d[0])!==s.pixelRatio[0]||(null==(c=t.pixelRatio)?void 0:c[1])!==s.pixelRatio[1]){t.width=s.width,t.height=s.height,t.pixelRatio=s.pixelRatio,t.sps=[e],t.duration=r;const i=e.subarray(1,4);let n="avc1.";for(let t=0;t<3;t++){let e=i[t].toString(16);e.length<2&&(e="0"+e),n+=e}t.codec=n}break}case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,o&&this.pushAccessUnit(o,t),o=this.VideoSample=this.createVideoSample(!1,s.pts,s.dts,"");break;case 12:a=!0;break;default:a=!1,o&&(o.debug+="unknown NAL "+i.type+" ")}if(o&&a){o.units.push(i)}})),i&&o&&(this.pushAccessUnit(o,t),this.VideoSample=null)}parseAVCNALu(t,e){const s=e.byteLength;let i=t.naluState||0;const r=i,n=[];let a,o,l,h=0,d=-1,c=0;for(-1===i&&(d=0,c=31&e[0],i=0,h=1);h=0){const t={data:e.subarray(d,o),type:c};n.push(t)}else{const s=this.getLastNalUnit(t.samples);s&&(r&&h<=4-r&&s.state&&(s.data=s.data.subarray(0,s.data.byteLength-r)),o>0&&(s.data=Bt(s.data,e.subarray(0,o)),s.state=0))}h=0&&i>=0){const t={data:e.subarray(d,s),type:c,state:i};n.push(t)}if(0===n.length){const s=this.getLastNalUnit(t.samples);s&&(s.data=Bt(s.data,e))}return t.naluState=i,n}}class tr{constructor(t,e,s){this.keyData=void 0,this.decrypter=void 0,this.keyData=s,this.decrypter=new di(e,{removePKCS7Padding:!1})}decryptBuffer(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)}decryptAacSample(t,e,s){const i=t[e].unit;if(i.length<=16)return;const r=i.subarray(16,i.length-i.length%16),n=r.buffer.slice(r.byteOffset,r.byteOffset+r.length);this.decryptBuffer(n).then((r=>{const n=new Uint8Array(r);i.set(n,16),this.decrypter.isSync()||this.decryptAacSamples(t,e+1,s)}))}decryptAacSamples(t,e,s){for(;;e++){if(e>=t.length)return void s();if(!(t[e].unit.length<32)&&(this.decryptAacSample(t,e,s),!this.decrypter.isSync()))return}}getAvcEncryptedData(t){const e=16*Math.floor((t.length-48)/160)+16,s=new Int8Array(e);let i=0;for(let e=32;e{r.data=this.getAvcDecryptedUnit(n,a),this.decrypter.isSync()||this.decryptAvcSamples(t,e,s+1,i)}))}decryptAvcSamples(t,e,s,i){if(t instanceof Uint8Array)throw new Error("Cannot decrypt samples of type Uint8Array");for(;;e++,s=0){if(e>=t.length)return void i();const r=t[e].units;for(;!(s>=r.length);s++){const n=r[s];if(!(n.data.length<=48||1!==n.type&&5!==n.type||(this.decryptAvcSample(t,e,s,i,n),this.decrypter.isSync())))return}}}}const er=188;class sr{constructor(t,e,s){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=s,this.videoParser=new Zi}static probe(t){const e=sr.syncOffset(t);return e>0&&A.warn(`MPEG2-TS detected but first sync word found @ offset ${e}`),-1!==e}static syncOffset(t){const e=t.length;let s=Math.min(940,e-er)+1,i=0;for(;i1&&(0===n&&a>2||o+er>s))return n}i++}return-1}static createTrack(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:At[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}}resetInitSegment(t,e,s,i){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=sr.createTrack("video"),this._audioTrack=sr.createTrack("audio",i),this._id3Track=sr.createTrack("id3"),this._txtTrack=sr.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=e,this.videoCodec=s,this._duration=i}resetTimeStamp(){}resetContiguity(){const{_audioTrack:t,_videoTrack:e,_id3Track:s}=this;t&&(t.pesData=null),e&&(e.pesData=null),s&&(s.pesData=null),this.aacOverFlow=null,this.remainderData=null}demux(t,e,s=!1,i=!1){let r;s||(this.sampleAes=null);const n=this._videoTrack,a=this._audioTrack,o=this._id3Track,l=this._txtTrack;let h=n.pid,d=n.pesData,c=a.pid,u=o.pid,f=a.pesData,g=o.pesData,m=null,p=this.pmtParsed,v=this._pmtId,y=t.length;if(this.remainderData&&(y=(t=Bt(this.remainderData,t)).length,this.remainderData=null),y>4>1){if(T=e+5+t[e+4],T===e+er)continue}else T=e+4;switch(y){case h:i&&(d&&(r=lr(d))&&this.videoParser.parseAVCPES(n,l,r,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(t.subarray(T,e+er)),d.size+=e+er-T);break;case c:if(i){if(f&&(r=lr(f)))switch(a.segmentCodec){case"aac":this.parseAACPES(a,r);break;case"mp3":this.parseMPEGPES(a,r);break;case"ac3":this.parseAC3PES(a,r)}f={data:[],size:0}}f&&(f.data.push(t.subarray(T,e+er)),f.size+=e+er-T);break;case u:i&&(g&&(r=lr(g))&&this.parseID3PES(o,r),g={data:[],size:0}),g&&(g.data.push(t.subarray(T,e+er)),g.size+=e+er-T);break;case 0:i&&(T+=t[T]+1),v=this._pmtId=rr(t,T);break;case v:{i&&(T+=t[T]+1);const r=nr(t,T,this.typeSupported,s,this.observer);h=r.videoPid,h>0&&(n.pid=h,n.segmentCodec=r.segmentVideoCodec),c=r.audioPid,c>0&&(a.pid=c,a.segmentCodec=r.segmentAudioCodec),u=r.id3Pid,u>0&&(o.pid=u),null===m||p||(A.warn(`MPEG-TS PMT found at ${e} after unknown PID '${m}'. Backtracking to sync byte @${E} to parse all TS packets.`),m=null,e=E-188),p=this.pmtParsed=!0;break}case 17:case 8191:break;default:m=y}}else T++;T>0&&ar(this.observer,new Error(`Found ${T} TS packet/s that do not start with 0x47`)),n.pesData=d,a.pesData=f,o.pesData=g;const S={audioTrack:a,videoTrack:n,id3Track:o,textTrack:l};return i&&this.extractRemainingSamples(S),S}flush(){const{remainderData:t}=this;let e;return this.remainderData=null,e=t?this.demux(t,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(e),this.sampleAes?this.decrypt(e,this.sampleAes):e}extractRemainingSamples(t){const{audioTrack:e,videoTrack:s,id3Track:i,textTrack:r}=t,n=s.pesData,a=e.pesData,o=i.pesData;let l;if(n&&(l=lr(n))?(this.videoParser.parseAVCPES(s,r,l,!0,this._duration),s.pesData=null):s.pesData=n,a&&(l=lr(a))){switch(e.segmentCodec){case"aac":this.parseAACPES(e,l);break;case"mp3":this.parseMPEGPES(e,l);break;case"ac3":this.parseAC3PES(e,l)}e.pesData=null}else null!=a&&a.size&&A.log("last AAC PES packet truncated,might overlap between fragments"),e.pesData=a;o&&(l=lr(o))?(this.parseID3PES(i,l),i.pesData=null):i.pesData=o}demuxSampleAes(t,e,s){const i=this.demux(t,s,!0,!this.config.progressive),r=this.sampleAes=new tr(this.observer,this.config,e);return this.decrypt(i,r)}decrypt(t,e){return new Promise((s=>{const{audioTrack:i,videoTrack:r}=t;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(()=>{r.samples?e.decryptAvcSamples(r.samples,0,0,(()=>{s(t)})):s(t)})):r.samples&&e.decryptAvcSamples(r.samples,0,0,(()=>{s(t)}))}))}destroy(){this._duration=0}parseAACPES(t,e){let s=0;const i=this.aacOverFlow;let r,n,a,o=e.data;if(i){this.aacOverFlow=null;const e=i.missing,r=i.sample.unit.byteLength;if(-1===e)o=Bt(i.sample.unit,o);else{const n=r-e;i.sample.unit.set(o.subarray(0,e),n),t.samples.push(i.sample),s=i.missing}}for(r=s,n=o.length;r0;)o+=n}}parseID3PES(t,e){if(void 0===e.pts)return void A.warn("[tsdemuxer]: ID3 PES unknown PTS");const s=u({},e,{type:this._videoTrack?Ge:Be,duration:Number.POSITIVE_INFINITY});t.samples.push(s)}}function ir(t,e){return((31&t[e+1])<<8)+t[e+2]}function rr(t,e){return(31&t[e+10])<<8|t[e+11]}function nr(t,e,s,i,r){const n={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},a=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0){let i=e+5,r=o;for(;r>2;){if(106===t[i])!0!==s.ac3?A.log("AC-3 audio found, not supported in this browser for now"):(n.audioPid=a,n.segmentAudioCodec="ac3");const e=t[i+1]+2;i+=e,r-=e}}break;case 194:case 135:return ar(r,new Error("Unsupported EC-3 in M2TS found")),n;case 36:return ar(r,new Error("Unsupported HEVC in M2TS found")),n}e+=o+5}return n}function ar(t,e,s){A.warn(`parsing error: ${e.message}`),t.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,levelRetry:s,error:e,reason:e.message})}function or(t){A.log(`${t} with AES-128-CBC encryption found in unencrypted stream`)}function lr(t){let e,s,i,r,n,a=0;const o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Bt(o[0],o[1]),o.splice(1,1);e=o[0];if(1===(e[0]<<16)+(e[1]<<8)+e[2]){if(s=(e[4]<<8)+e[5],s&&s>t.size-6)return null;const l=e[7];192&l&&(r=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?(n=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2,r-n>54e5&&(A.warn(`${Math.round((r-n)/9e4)}s delta between PTS and DTS, align them`),r=n)):n=r),i=e[8];let h=i+9;if(t.size<=h)return null;t.size-=h;const d=new Uint8Array(t.size);for(let t=0,s=o.length;ts){h-=s;continue}e=e.subarray(h),s-=h,h=0}d.set(e,a),a+=s}return s&&(s-=i+3),{data:d,pts:r,dts:n,len:s}}return null}class hr{static getSilentFrame(t,e){if("mp4a.40.2"===t){if(1===e)return new Uint8Array([0,200,0,128,35,128]);if(2===e)return new Uint8Array([33,0,73,144,2,25,0,35,128]);if(3===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,142]);if(4===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,128,44,128,8,2,56]);if(5===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,56]);if(6===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,0,178,0,32,8,224])}else{if(1===e)return new Uint8Array([1,64,34,128,163,78,230,128,186,8,0,0,0,28,6,241,193,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(2===e)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(3===e)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94])}}}const dr=Math.pow(2,32)-1;class cr{static init(){let t;for(t in cr.types={avc1:[],avcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],".mp3":[],dac3:[],"ac-3":[],mvex:[],mvhd:[],pasp:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[]},cr.types)cr.types.hasOwnProperty(t)&&(cr.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);const e=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),s=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]);cr.HDLR_TYPES={video:e,audio:s};const i=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),r=new Uint8Array([0,0,0,0,0,0,0,0]);cr.STTS=cr.STSC=cr.STCO=r,cr.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),cr.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0]),cr.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),cr.STSD=new Uint8Array([0,0,0,0,0,0,0,1]);const n=new Uint8Array([105,115,111,109]),a=new Uint8Array([97,118,99,49]),o=new Uint8Array([0,0,0,1]);cr.FTYP=cr.box(cr.types.ftyp,n,o,n,a),cr.DINF=cr.box(cr.types.dinf,cr.box(cr.types.dref,i))}static box(t,...e){let s=8,i=e.length;const r=i;for(;i--;)s+=e[i].byteLength;const n=new Uint8Array(s);for(n[0]=s>>24&255,n[1]=s>>16&255,n[2]=s>>8&255,n[3]=255&s,n.set(t,4),i=0,s=8;i>24&255,t>>16&255,t>>8&255,255&t,s>>24,s>>16&255,s>>8&255,255&s,i>>24,i>>16&255,i>>8&255,255&i,85,196,0,0]))}static mdia(t){return cr.box(cr.types.mdia,cr.mdhd(t.timescale,t.duration),cr.hdlr(t.type),cr.minf(t))}static mfhd(t){return cr.box(cr.types.mfhd,new Uint8Array([0,0,0,0,t>>24,t>>16&255,t>>8&255,255&t]))}static minf(t){return"audio"===t.type?cr.box(cr.types.minf,cr.box(cr.types.smhd,cr.SMHD),cr.DINF,cr.stbl(t)):cr.box(cr.types.minf,cr.box(cr.types.vmhd,cr.VMHD),cr.DINF,cr.stbl(t))}static moof(t,e,s){return cr.box(cr.types.moof,cr.mfhd(t),cr.traf(s,e))}static moov(t){let e=t.length;const s=[];for(;e--;)s[e]=cr.trak(t[e]);return cr.box.apply(null,[cr.types.moov,cr.mvhd(t[0].timescale,t[0].duration)].concat(s).concat(cr.mvex(t)))}static mvex(t){let e=t.length;const s=[];for(;e--;)s[e]=cr.trex(t[e]);return cr.box.apply(null,[cr.types.mvex,...s])}static mvhd(t,e){e*=t;const s=Math.floor(e/(dr+1)),i=Math.floor(e%(dr+1)),r=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,t>>24&255,t>>16&255,t>>8&255,255&t,s>>24,s>>16&255,s>>8&255,255&s,i>>24,i>>16&255,i>>8&255,255&i,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return cr.box(cr.types.mvhd,r)}static sdtp(t){const e=t.samples||[],s=new Uint8Array(4+e.length);let i,r;for(i=0;i>>8&255),r.push(255&i),r=r.concat(Array.prototype.slice.call(s));for(e=0;e>>8&255),n.push(255&i),n=n.concat(Array.prototype.slice.call(s));const a=cr.box(cr.types.avcC,new Uint8Array([1,r[3],r[4],r[5],255,224|t.sps.length].concat(r).concat([t.pps.length]).concat(n))),o=t.width,l=t.height,h=t.pixelRatio[0],d=t.pixelRatio[1];return cr.box(cr.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,o>>8&255,255&o,l>>8&255,255&l,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),a,cr.box(cr.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),cr.box(cr.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))}static esds(t){const e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))}static audioStsd(t){const e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])}static mp4a(t){return cr.box(cr.types.mp4a,cr.audioStsd(t),cr.box(cr.types.esds,cr.esds(t)))}static mp3(t){return cr.box(cr.types[".mp3"],cr.audioStsd(t))}static ac3(t){return cr.box(cr.types["ac-3"],cr.audioStsd(t),cr.box(cr.types.dac3,t.config))}static stsd(t){return"audio"===t.type?"mp3"===t.segmentCodec&&"mp3"===t.codec?cr.box(cr.types.stsd,cr.STSD,cr.mp3(t)):"ac3"===t.segmentCodec?cr.box(cr.types.stsd,cr.STSD,cr.ac3(t)):cr.box(cr.types.stsd,cr.STSD,cr.mp4a(t)):cr.box(cr.types.stsd,cr.STSD,cr.avc1(t))}static tkhd(t){const e=t.id,s=t.duration*t.timescale,i=t.width,r=t.height,n=Math.floor(s/(dr+1)),a=Math.floor(s%(dr+1));return cr.box(cr.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n,a>>24,a>>16&255,a>>8&255,255&a,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,i>>8&255,255&i,0,0,r>>8&255,255&r,0,0]))}static traf(t,e){const s=cr.sdtp(t),i=t.id,r=Math.floor(e/(dr+1)),n=Math.floor(e%(dr+1));return cr.box(cr.types.traf,cr.box(cr.types.tfhd,new Uint8Array([0,0,0,0,i>>24,i>>16&255,i>>8&255,255&i])),cr.box(cr.types.tfdt,new Uint8Array([1,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,n>>24,n>>16&255,n>>8&255,255&n])),cr.trun(t,s.length+16+20+8+16+8+8),s)}static trak(t){return t.duration=t.duration||4294967295,cr.box(cr.types.trak,cr.tkhd(t),cr.mdia(t))}static trex(t){const e=t.id;return cr.box(cr.types.trex,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))}static trun(t,e){const s=t.samples||[],i=s.length,r=12+16*i,n=new Uint8Array(r);let a,o,l,h,d,c;for(e+=8+r,n.set(["video"===t.type?1:0,0,15,1,i>>>24&255,i>>>16&255,i>>>8&255,255&i,e>>>24&255,e>>>16&255,e>>>8&255,255&e],0),a=0;a>>24&255,l>>>16&255,l>>>8&255,255&l,h>>>24&255,h>>>16&255,h>>>8&255,255&h,d.isLeading<<2|d.dependsOn,d.isDependedOn<<6|d.hasRedundancy<<4|d.paddingValue<<1|d.isNonSync,61440&d.degradPrio,15&d.degradPrio,c>>>24&255,c>>>16&255,c>>>8&255,255&c],12+16*a);return cr.box(cr.types.trun,n)}static initSegment(t){cr.types||cr.init();const e=cr.moov(t);return Bt(cr.FTYP,e)}}cr.types=void 0,cr.HDLR_TYPES=void 0,cr.STTS=void 0,cr.STSC=void 0,cr.STCO=void 0,cr.STSZ=void 0,cr.VMHD=void 0,cr.SMHD=void 0,cr.STSD=void 0,cr.FTYP=void 0,cr.DINF=void 0;function ur(t,e,s=1,i=!1){const r=t*e*s;return i?Math.round(r):r}function fr(t,e=!1){return ur(t,1e3,1/9e4,e)}let gr,mr=null,pr=null;class vr{constructor(t,e,s,i=""){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=s,this.ISGenerated=!1,null===mr){const t=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);mr=t?parseInt(t[1]):0}if(null===pr){const t=navigator.userAgent.match(/Safari\/(\d+)/i);pr=t?parseInt(t[1]):0}}destroy(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null}resetTimeStamp(t){A.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t}resetNextTimestamp(){A.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1}resetInitSegment(){A.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0}getVideoStartPts(t){let e=!1;const s=t.reduce(((t,s)=>{const i=s.pts-t;return i<-4294967296?(e=!0,yr(t,s.pts)):i>0?t:s.pts}),t[0].pts);return e&&A.debug("PTS rollover detected"),s}remux(t,e,s,i,r,n,a,o){let l,h,d,c,u,f,g=r,m=r;const p=t.pid>-1,v=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=a&&y>0||y>1;if((!p||E)&&(!v||T)||this.ISGenerated||a){if(this.ISGenerated){var S,L,R,b;const t=this.videoTrackConfig;!t||e.width===t.width&&e.height===t.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=t.pixelRatio)?void 0:L[0])&&(null==(R=e.pixelRatio)?void 0:R[1])===(null==(b=t.pixelRatio)?void 0:b[1])||this.resetInitSegment()}else d=this.generateIS(t,e,r,n);const s=this.isVideoContiguous;let i,a=-1;if(T&&(a=function(t){for(let e=0;e0){A.warn(`[mp4-remuxer]: Dropped ${a} out of ${y} video samples due to a missing keyframe`);const t=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(a),e.dropped+=a,m+=(e.samples[0].pts-t)/e.inputTimeScale,i=m}else-1===a&&(A.warn(`[mp4-remuxer]: No keyframe found out of ${y} video samples`),f=!1);if(this.ISGenerated){if(E&&T){const s=this.getVideoStartPts(e.samples),i=(yr(t.samples[0].pts,s)-s)/e.inputTimeScale;g+=Math.max(0,i),m+=Math.max(0,-i)}if(E){if(t.samplerate||(A.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),d=this.generateIS(t,e,r,n)),h=this.remuxAudio(t,g,this.isAudioContiguous,n,v||T||o===Ie?m:void 0),T){const i=h?h.endPTS-h.startPTS:0;e.inputTimeScale||(A.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),d=this.generateIS(t,e,r,n)),l=this.remuxVideo(e,m,s,i)}}else T&&(l=this.remuxVideo(e,m,s,0));l&&(l.firstKeyFrame=a,l.independent=-1!==a,l.firstKeyFramePTS=i)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(s.samples.length&&(u=Er(s,r,this._initPTS,this._initDTS)),i.samples.length&&(c=Tr(i,r,this._initPTS))),{audio:h,video:l,initSegment:d,independent:f,text:c,id3:u}}generateIS(t,e,s,i){const r=t.samples,n=e.samples,a=this.typeSupported,o={},l=this._initPTS;let h,d,c,u=!l||i,f="audio/mp4";if(u&&(h=d=1/0),t.config&&r.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":a.mpeg?(f="audio/mpeg",t.codec=""):a.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}o.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&a.mpeg?new Uint8Array(0):cr.initSegment([t]),metadata:{channelCount:t.channelCount}},u&&(c=t.inputTimeScale,l&&c===l.timescale?u=!1:h=d=r[0].pts-Math.round(c*s))}if(e.sps&&e.pps&&n.length){if(e.timescale=e.inputTimeScale,o.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:cr.initSegment([e]),metadata:{width:e.width,height:e.height}},u)if(c=e.inputTimeScale,l&&c===l.timescale)u=!1;else{const t=this.getVideoStartPts(n),e=Math.round(c*s);d=Math.min(d,yr(n[0].dts,t)-e),h=Math.min(h,t-e)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(o).length)return this.ISGenerated=!0,u?(this._initPTS={baseTime:h,timescale:c},this._initDTS={baseTime:d,timescale:c}):h=c=void 0,{tracks:o,initPTS:h,timescale:c}}remuxVideo(t,e,s,i){const r=t.inputTimeScale,n=t.samples,a=[],o=n.length,l=this._initPTS;let h,d,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,m=Number.POSITIVE_INFINITY,E=Number.NEGATIVE_INFINITY,T=!1;if(!s||null===c){const t=e*r,i=n[0].pts-yr(n[0].dts,n[0].pts);mr&&null!==c&&Math.abs(t-i-c)<15e3?s=!0:c=t-i}const S=l.baseTime*r/l.timescale;for(let t=0;t0?t-1:t].dts&&(T=!0)}T&&n.sort((function(t,e){const s=t.dts-e.dts,i=t.pts-e.pts;return s||i})),h=n[0].dts,d=n[n.length-1].dts;const L=d-h,R=L?Math.round(L/(o-1)):g||t.inputTimeScale/30;if(s){const t=h-c,s=t>R,i=t<-1;if((s||i)&&(s?A.warn(`AVC: ${fr(t,!0)} ms (${t}dts) hole between fragments detected at ${e.toFixed(3)}`):A.warn(`AVC: ${fr(-t,!0)} ms (${t}dts) overlapping between fragments detected at ${e.toFixed(3)}`),!i||c>=n[0].pts||mr)){h=c;const e=n[0].pts-t;if(s)n[0].dts=h,n[0].pts=e;else for(let s=0;se);s++)n[s].dts-=t,n[s].pts-=t;A.log(`Video: Initial PTS/DTS adjusted: ${fr(e,!0)}/${fr(h,!0)}, delta: ${fr(t,!0)} ms`)}}h=Math.max(0,h);let b=0,k=0,w=h;for(let t=0;t0?e.dts-n[t-1].dts:R;if(l=t>0?e.pts-n[t-1].pts:R,s.stretchShortVideoTrack&&null!==this.nextAudioPts){const t=Math.floor(s.maxBufferHole*r),n=(i?m+i*r:this.nextAudioPts)-e.pts;n>t?(g=n-a,g<0?g=a:C=!0,A.log(`[mp4-remuxer]: It is approximately ${n/90} ms to the next segment; using duration ${g/90} ms for the last video frame.`)):g=a}else g=a}const d=Math.round(e.pts-e.dts);x=Math.min(x,g),M=Math.max(M,g),P=Math.min(P,l),F=Math.max(F,l),a.push(new Sr(e.key,g,h,d))}if(a.length)if(mr){if(mr<70){const t=a[0].flags;t.dependsOn=2,t.isNonSync=0}}else if(pr&&F-P0&&(i&&Math.abs(T-E)<9e3||Math.abs(yr(g[0].pts-S,T)-E)<20*l),g.forEach((function(t){t.pts=yr(t.pts-S,T)})),!s||E<0){if(g=g.filter((t=>t.pts>=0)),!g.length)return;E=0===r?0:i&&!f?Math.max(0,T):g[0].pts}if("aac"===t.segmentCodec){const e=this.config.maxAudioFramesDrift;for(let s=0,i=E;s=e*l&&h<1e4&&f){let e=Math.round(o/l);i=a-e*l,i<0&&(e--,i+=l),0===s&&(this.nextAudioPts=E=i),A.warn(`[mp4-remuxer]: Injecting ${e} audio frame @ ${(i/n).toFixed(3)}s due to ${Math.round(1e3*o/n)} ms gap.`);for(let n=0;n0))return;k+=m;try{L=new Uint8Array(k)}catch(t){return void this.observer.emit(p.ERROR,p.ERROR,{type:v.MUX_ERROR,details:y.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:k,reason:`fail allocating audio mdat ${k}`})}if(!d){new DataView(L.buffer).setUint32(0,k),L.set(cr.types.mdat,4)}}L.set(r,m);const l=r.byteLength;m+=l,c.push(new Sr(!0,o,l,0)),b=n}const D=c.length;if(!D)return;const I=c[c.length-1];this.nextAudioPts=E=b+a*I.duration;const _=d?new Uint8Array(0):cr.moof(t.sequenceNumber++,R/a,u({},t,{samples:c}));t.samples=[];const C=R/n,x=E/n,P={data1:_,data2:L,startPTS:C,endPTS:x,startDTS:C,endDTS:x,type:"audio",hasAudio:!0,hasVideo:!1,nb:D};return this.isAudioContiguous=!0,P}remuxEmptyAudio(t,e,s,i){const r=t.inputTimeScale,n=r/(t.samplerate?t.samplerate:r),a=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,h=(null!==a?a:i.startDTS*r)+l,d=i.endDTS*r+l,c=1024*n,u=Math.ceil((d-h)/c),f=hr.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(A.warn("[mp4-remuxer]: remux empty Audio"),!f)return void A.trace("[mp4-remuxer]: Unable to remuxEmptyAudio since we were unable to get a silent frame for given audio codec");const g=[];for(let t=0;t4294967296;)t+=s;return t}function Er(t,e,s,i){const r=t.samples.length;if(!r)return;const n=t.inputTimeScale;for(let a=0;at.pts-e.pts));const n=t.samples;return t.samples=[],{samples:n}}class Sr{constructor(t,e,s,i){this.size=void 0,this.duration=void 0,this.cts=void 0,this.flags=void 0,this.duration=e,this.size=s,this.cts=i,this.flags={isLeading:0,isDependedOn:0,hasRedundancy:0,degradPrio:0,dependsOn:t?2:1,isNonSync:t?0:1}}}function Lr(t,e){const s=null==t?void 0:t.codec;if(s&&s.length>4)return s;if(e===_){if("ec-3"===s||"ac-3"===s||"alac"===s)return s;if("fLaC"===s||"Opus"===s){return he(s,!1)}const t="mp4a.40.5";return A.info(`Parsed audio codec "${s}" or audio object type not handled. Using "${t}"`),t}return A.warn(`Unhandled video codec "${s}"`),"hvc1"===s||"hev1"===s?"hvc1.1.6.L120.90":"av01"===s?"av01.0.04M.08":"avc1.42e01e"}try{gr=self.performance.now.bind(self.performance)}catch(t){A.debug("Unable to use Performance API on this environment"),gr=null==$?void 0:$.Date.now}const Ar=[{demux:class{constructor(t,e){this.remainderData=null,this.timeOffset=0,this.config=void 0,this.videoTrack=void 0,this.audioTrack=void 0,this.id3Track=void 0,this.txtTrack=void 0,this.config=e}resetTimeStamp(){}resetInitSegment(t,e,s,i){const r=this.videoTrack=ki("video",1),n=this.audioTrack=ki("audio",1),a=this.txtTrack=ki("text",1);if(this.id3Track=ki("id3",1),this.timeOffset=0,null==t||!t.byteLength)return;const o=xt(t);if(o.video){const{id:t,timescale:e,codec:s}=o.video;r.id=t,r.timescale=a.timescale=e,r.codec=s}if(o.audio){const{id:t,timescale:e,codec:s}=o.audio;n.id=t,n.timescale=e,n.codec=s}a.id=At.text,r.sampleDuration=0,r.duration=n.duration=i}resetContiguity(){this.remainderData=null}static probe(t){return function(t){const e=t.byteLength;for(let s=0;s8&&109===t[s+4]&&111===t[s+5]&&111===t[s+6]&&102===t[s+7])return!0;s=i>1?s+i:e}return!1}(t)}demux(t,e){this.timeOffset=e;let s=t;const i=this.videoTrack,r=this.txtTrack;if(this.config.progressive){this.remainderData&&(s=Bt(this.remainderData,t));const e=function(t){const e={valid:null,remainder:null},s=_t(t,["moof"]);if(s.length<2)return e.remainder=t,e;const i=s[s.length-1];return e.valid=st(t,0,i.byteOffset-8),e.remainder=st(t,i.byteOffset-8),e}(s);this.remainderData=e.remainder,i.samples=e.valid||new Uint8Array}else i.samples=s;const n=this.extractID3Track(i,e);return r.samples=$t(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:n,textTrack:this.txtTrack}}flush(){const t=this.timeOffset,e=this.videoTrack,s=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;const i=this.extractID3Track(e,this.timeOffset);return s.samples=$t(t,e),{videoTrack:e,audioTrack:ki(),id3Track:i,textTrack:ki()}}extractID3Track(t,e){const s=this.id3Track;if(t.samples.length){const i=_t(t.samples,["emsg"]);i&&i.forEach((t=>{const i=function(t){const e=t[0];let s="",i="",r=0,n=0,a=0,o=0,l=0,h=0;if(0===e){for(;"\0"!==Rt(t.subarray(h,h+1));)s+=Rt(t.subarray(h,h+1)),h+=1;for(s+=Rt(t.subarray(h,h+1)),h+=1;"\0"!==Rt(t.subarray(h,h+1));)i+=Rt(t.subarray(h,h+1)),h+=1;i+=Rt(t.subarray(h,h+1)),h+=1,r=kt(t,12),n=kt(t,16),o=kt(t,20),l=kt(t,24),h=28}else if(1===e){h+=4,r=kt(t,h),h+=4;const e=kt(t,h);h+=4;const n=kt(t,h);for(h+=4,a=2**32*e+n,g(a)||(a=Number.MAX_SAFE_INTEGER,A.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=kt(t,h),h+=4,l=kt(t,h),h+=4;"\0"!==Rt(t.subarray(h,h+1));)s+=Rt(t.subarray(h,h+1)),h+=1;for(s+=Rt(t.subarray(h,h+1)),h+=1;"\0"!==Rt(t.subarray(h,h+1));)i+=Rt(t.subarray(h,h+1)),h+=1;i+=Rt(t.subarray(h,h+1)),h+=1}return{schemeIdUri:s,value:i,timeScale:r,presentationTime:a,presentationTimeDelta:n,eventDuration:o,id:l,payload:t.subarray(h,t.byteLength)}}(t);if(ji.test(i.schemeIdUri)){const t=f(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale;let r=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;r<=.001&&(r=Number.POSITIVE_INFINITY);const n=i.payload;s.samples.push({data:n,len:n.byteLength,dts:t,pts:t,type:Ge,duration:r})}}))}return s}demuxSampleAes(t,e,s){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))}destroy(){}},remux:class{constructor(){this.emitInitSegment=!1,this.audioCodec=void 0,this.videoCodec=void 0,this.initData=void 0,this.initPTS=null,this.initTracks=void 0,this.lastEndTime=null}destroy(){}resetTimeStamp(t){this.initPTS=t,this.lastEndTime=null}resetNextTimestamp(){this.lastEndTime=null}resetInitSegment(t,e,s,i){this.audioCodec=e,this.videoCodec=s,this.generateInitSegment(function(t,e){if(!t||!e)return t;const s=e.keyId;s&&e.isCommonEncryption&&_t(t,["moov","trak"]).forEach((t=>{const e=_t(t,["mdia","minf","stbl","stsd"])[0].subarray(8);let i=_t(e,["enca"]);const r=i.length>0;r||(i=_t(e,["encv"])),i.forEach((t=>{_t(r?t.subarray(28):t.subarray(78),["sinf"]).forEach((t=>{const e=Nt(t);if(e){const t=e.subarray(8,24);t.some((t=>0!==t))||(A.log(`[eme] Patching keyId in 'enc${r?"a":"v"}>sinf>>tenc' box: ${Tt(t)} -> ${Tt(s)}`),e.set(s,8))}}))}))}));return t}(t,i)),this.emitInitSegment=!0}generateInitSegment(t){let{audioCodec:e,videoCodec:s}=this;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);const i=this.initData=xt(t);i.audio&&(e=Lr(i.audio,_)),i.video&&(s=Lr(i.video,C));const r={};i.audio&&i.video?r.audiovideo={container:"video/mp4",codec:e+","+s,initSegment:t,id:"main"}:i.audio?r.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?r.video={container:"video/mp4",codec:s,initSegment:t,id:"main"}:A.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=r}remux(t,e,s,i,r,n){var a,o;let{initPTS:l,lastEndTime:h}=this;const d={audio:void 0,video:void 0,text:i,id3:s,initSegment:void 0};f(h)||(h=this.lastEndTime=r||0);const c=e.samples;if(null==c||!c.length)return d;const u={initPTS:void 0,timescale:1};let g=this.initData;if(null!=(a=g)&&a.length||(this.generateInitSegment(c),g=this.initData),null==(o=g)||!o.length)return A.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),d;this.emitInitSegment&&(u.tracks=this.initTracks,this.emitInitSegment=!1);const m=function(t,e){let s=0,i=0,r=0;const n=_t(t,["moof","traf"]);for(let t=0;tt+e.info.duration||0),0);s=Math.max(s,t+n.earliestPresentationTime/n.timescale),i=s-e}}if(i&&f(i))return i}return i||r}(c,g),p=function(t,e){return _t(e,["moof","traf"]).reduce(((e,s)=>{const i=_t(s,["tfdt"])[0],r=i[0],n=_t(s,["tfhd"]).reduce(((e,s)=>{const n=kt(s,4),a=t[n];if(a){let t=kt(i,4);if(1===r){if(t===St)return A.warn("[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time"),e;t*=St+1,t+=kt(i,8)}const s=t/(a.timescale||9e4);if(f(s)&&(null===e||sr}(l,v,r,m)||u.timescale!==l.timescale&&n)&&(u.initPTS=v-r,l&&1===l.timescale&&A.warn("Adjusting initPTS by "+(u.initPTS-l.baseTime)),this.initPTS=l={baseTime:u.initPTS,timescale:1});const y=t?v-l.baseTime/l.timescale:h,E=y+m;!function(t,e,s){_t(e,["moof","traf"]).forEach((e=>{_t(e,["tfhd"]).forEach((i=>{const r=kt(i,4),n=t[r];if(!n)return;const a=n.timescale||9e4;_t(e,["tfdt"]).forEach((t=>{const e=t[0],i=s*a;if(i){let s=kt(t,4);if(0===e)s-=i,s=Math.max(s,0),It(t,4,s);else{s*=Math.pow(2,32),s+=kt(t,8),s-=i,s=Math.max(s,0);const e=Math.floor(s/(St+1)),r=Math.floor(s%(St+1));It(t,4,e),It(t,8,r)}}}))}))}))}(g,c,l.baseTime/l.timescale),m>0?this.lastEndTime=E:(A.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());const T=!!g.audio,S=!!g.video;let L="";T&&(L+="audio"),S&&(L+="video");const R={data1:c,startPTS:y,startDTS:y,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return d.audio="audio"===R.type?R:void 0,d.video="audio"!==R.type?R:void 0,d.initSegment=u,d.id3=Er(s,r,l,l),i.samples.length&&(d.text=Tr(i,r,l)),d}}},{demux:sr,remux:vr},{demux:class extends wi{constructor(t,e){super(),this.observer=void 0,this.config=void 0,this.observer=t,this.config=e}resetInitSegment(t,e,s,i){super.resetInitSegment(t,e,s,i),this._audioTrack={container:"audio/adts",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"aac",samples:[],manifestCodec:e,duration:i,inputTimeScale:9e4,dropped:0}}static probe(t){if(!t)return!1;const e=nt(t,0);let s=(null==e?void 0:e.length)||0;if(Wi(t,s))return!1;for(let e=t.length;s0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(s=e);return s}(n,e);if(L&&"AES-128"===L.method){const t=this.getDecrypter();if(!t.isSync())return this.decryptionPromise=t.webCryptoDecrypt(n,L.key.buffer,L.iv.buffer).then((t=>{const e=this.push(t,null,s);return this.decryptionPromise=null,e})),this.decryptionPromise;{let e=t.softwareDecrypt(n,L.key.buffer,L.iv.buffer);if(s.part>-1&&(e=t.flush()),!e)return r.executeEnd=gr(),br(s);n=new Uint8Array(e)}}const R=this.needsProbing(h,d);if(R){const t=this.configureTransmuxer(n);if(t)return A.warn(`[transmuxer] ${t.message}`),this.observer.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,error:t,reason:t.message}),r.executeEnd=gr(),br(s)}(h||d||f||R)&&this.resetInitSegment(S,g,m,T,e),(h||f||R)&&this.resetInitialTimestamp(E),l||this.resetContiguity();const b=this.transmux(n,L,u,c,s),k=this.currentTransmuxState;return k.contiguous=!0,k.discontinuity=!1,k.trackSwitch=!1,r.executeEnd=gr(),b}flush(t){const e=t.transmuxing;e.executeStart=gr();const{decrypter:s,currentTransmuxState:i,decryptionPromise:r}=this;if(r)return r.then((()=>this.flush(t)));const n=[],{timeOffset:a}=i;if(s){const e=s.flush();e&&n.push(this.push(e,null,t))}const{demuxer:o,remuxer:l}=this;if(!o||!l)return e.executeEnd=gr(),[br(t)];const h=o.flush(a);return kr(h)?h.then((e=>(this.flushRemux(n,e,t),n))):(this.flushRemux(n,h,t),n)}flushRemux(t,e,s){const{audioTrack:i,videoTrack:r,id3Track:n,textTrack:a}=e,{accurateTimeOffset:o,timeOffset:l}=this.currentTransmuxState;A.log(`[transmuxer.ts]: Flushed fragment ${s.sn}${s.part>-1?" p: "+s.part:""} of level ${s.level}`);const h=this.remuxer.remux(i,r,n,a,l,o,!0,this.id);t.push({remuxResult:h,chunkMeta:s}),s.transmuxing.executeEnd=gr()}resetInitialTimestamp(t){const{demuxer:e,remuxer:s}=this;e&&s&&(e.resetTimeStamp(t),s.resetTimeStamp(t))}resetContiguity(){const{demuxer:t,remuxer:e}=this;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())}resetInitSegment(t,e,s,i,r){const{demuxer:n,remuxer:a}=this;n&&a&&(n.resetInitSegment(t,e,s,i),a.resetInitSegment(t,e,s,r))}destroy(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)}transmux(t,e,s,i,r){let n;return n=e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,s,i,r):this.transmuxUnencrypted(t,s,i,r),n}transmuxUnencrypted(t,e,s,i){const{audioTrack:r,videoTrack:n,id3Track:a,textTrack:o}=this.demuxer.demux(t,e,!1,!this.config.progressive);return{remuxResult:this.remuxer.remux(r,n,a,o,e,s,!1,this.id),chunkMeta:i}}transmuxSampleAes(t,e,s,i,r){return this.demuxer.demuxSampleAes(t,e,s).then((t=>({remuxResult:this.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,s,i,!1,this.id),chunkMeta:r})))}configureTransmuxer(t){const{config:e,observer:s,typeSupported:i,vendor:r}=this;let n;for(let e=0,s=Ar.length;e({remuxResult:{},chunkMeta:t});function kr(t){return"then"in t&&t.then instanceof Function}class wr{constructor(t,e,s,i,r){this.audioCodec=void 0,this.videoCodec=void 0,this.initSegmentData=void 0,this.duration=void 0,this.defaultInitPts=void 0,this.audioCodec=t,this.videoCodec=e,this.initSegmentData=s,this.duration=i,this.defaultInitPts=r||null}}class Dr{constructor(t,e,s,i,r,n){this.discontinuity=void 0,this.contiguous=void 0,this.accurateTimeOffset=void 0,this.trackSwitch=void 0,this.timeOffset=void 0,this.initSegmentChange=void 0,this.discontinuity=t,this.contiguous=e,this.accurateTimeOffset=s,this.trackSwitch=i,this.timeOffset=r,this.initSegmentChange=n}}var Ir={exports:{}};!function(t){var e=Object.prototype.hasOwnProperty,s="~";function i(){}function r(t,e,s){this.fn=t,this.context=e,this.once=s||!1}function n(t,e,i,n,a){if("function"!=typeof i)throw new TypeError("The listener must be a function");var o=new r(i,n||t,a),l=s?s+e:e;return t._events[l]?t._events[l].fn?t._events[l]=[t._events[l],o]:t._events[l].push(o):(t._events[l]=o,t._eventsCount++),t}function a(t,e){0==--t._eventsCount?t._events=new i:delete t._events[e]}function o(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(s=!1)),o.prototype.eventNames=function(){var t,i,r=[];if(0===this._eventsCount)return r;for(i in t=this._events)e.call(t,i)&&r.push(s?i.slice(1):i);return Object.getOwnPropertySymbols?r.concat(Object.getOwnPropertySymbols(t)):r},o.prototype.listeners=function(t){var e=s?s+t:t,i=this._events[e];if(!i)return[];if(i.fn)return[i.fn];for(var r=0,n=i.length,a=new Array(n);r{(e=e||{}).frag=this.frag,e.id=this.id,t===p.ERROR&&(this.error=e.error),this.hls.trigger(t,e)};this.observer=new _r,this.observer.on(p.FRAG_DECRYPTED,n),this.observer.on(p.ERROR,n);const a=te(r.preferManagedMediaSource)||{isTypeSupported:()=>!1},o={mpeg:a.isTypeSupported("audio/mpeg"),mp3:a.isTypeSupported('audio/mp4; codecs="mp3"'),ac3:a.isTypeSupported('audio/mp4; codecs="ac-3"')};if(this.useWorker&&"undefined"!=typeof Worker){if(r.workerPath||"function"==typeof __HLS_WORKER_BUNDLE__){try{r.workerPath?(A.log(`loading Web Worker ${r.workerPath} for "${e}"`),this.workerContext=function(t){const e=new self.URL(t,self.location.href).href;return{worker:new self.Worker(e),scriptURL:e}}(r.workerPath)):(A.log(`injecting Web Worker for "${e}"`),this.workerContext=function(){const t=new self.Blob([`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`],{type:"text/javascript"}),e=self.URL.createObjectURL(t);return{worker:new self.Worker(e),objectURL:e}}()),this.onwmsg=t=>this.onWorkerMessage(t);const{worker:t}=this.workerContext;t.addEventListener("message",this.onwmsg),t.onerror=t=>{const s=new Error(`${t.message} (${t.filename}:${t.lineno})`);r.enableWorker=!1,A.warn(`Error in "${e}" Web Worker, fallback to inline`),this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,fatal:!1,event:"demuxerWorker",error:s})},t.postMessage({cmd:"init",typeSupported:o,vendor:"",id:e,config:JSON.stringify(r)})}catch(t){A.warn(`Error setting up "${e}" Web Worker, fallback to inline`,t),this.resetWorker(),this.error=null,this.transmuxer=new Rr(this.observer,o,r,"",e)}return}}this.transmuxer=new Rr(this.observer,o,r,"",e)}resetWorker(){if(this.workerContext){const{worker:t,objectURL:e}=this.workerContext;e&&self.URL.revokeObjectURL(e),t.removeEventListener("message",this.onwmsg),t.onerror=null,t.terminate(),this.workerContext=null}}destroy(){if(this.workerContext)this.resetWorker(),this.onwmsg=void 0;else{const t=this.transmuxer;t&&(t.destroy(),this.transmuxer=null)}const t=this.observer;t&&t.removeAllListeners(),this.frag=null,this.observer=null,this.hls=null}push(t,e,s,i,r,n,a,o,l,h){var d,c;l.transmuxing.start=self.performance.now();const{transmuxer:u}=this,f=n?n.start:r.start,g=r.decryptdata,m=this.frag,p=!(m&&r.cc===m.cc),v=!(m&&l.level===m.level),y=m?l.sn-m.sn:-1,E=this.part?l.part-this.part.index:-1,T=0===y&&l.id>1&&l.id===(null==m?void 0:m.stats.chunkCount),S=!v&&(1===y||0===y&&(1===E||T&&E<=0)),L=self.performance.now();(v||y||0===r.stats.parsing.start)&&(r.stats.parsing.start=L),!n||!E&&S||(n.stats.parsing.start=L);const R=!(m&&(null==(d=r.initSegment)?void 0:d.url)===(null==(c=m.initSegment)?void 0:c.url)),b=new Dr(p,S,o,v,f,R);if(!S||p||R){A.log(`[transmuxer-interface, ${r.type}]: Starting new transmux session for sn: ${l.sn} p: ${l.part} level: ${l.level} id: ${l.id}\n discontinuity: ${p}\n trackSwitch: ${v}\n contiguous: ${S}\n accurateTimeOffset: ${o}\n timeOffset: ${f}\n initSegmentChange: ${R}`);const t=new wr(s,i,e,a,h);this.configureTransmuxer(t)}if(this.frag=r,this.part=n,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:g,chunkMeta:l,state:b},t instanceof ArrayBuffer?[t]:[]);else if(u){const e=u.push(t,g,l,b);kr(e)?(u.async=!0,e.then((t=>{this.handleTransmuxComplete(t)})).catch((t=>{this.transmuxerError(t,l,"transmuxer-interface push error")}))):(u.async=!1,this.handleTransmuxComplete(e))}}flush(t){t.transmuxing.start=self.performance.now();const{transmuxer:e}=this;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(e){let s=e.flush(t);kr(s)||e.async?(kr(s)||(s=Promise.resolve(s)),s.then((e=>{this.handleFlushResult(e,t)})).catch((e=>{this.transmuxerError(e,t,"transmuxer-interface flush error")}))):this.handleFlushResult(s,t)}}transmuxerError(t,e,s){this.hls&&(this.error=t,this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,chunkMeta:e,frag:this.frag||void 0,fatal:!1,error:t,err:t,reason:s}))}handleFlushResult(t,e){t.forEach((t=>{this.handleTransmuxComplete(t)})),this.onFlush(e)}onWorkerMessage(t){const e=t.data;if(null==e||!e.event)return void A.warn("worker message received with no "+(e?"event name":"data"));const s=this.hls;if(this.hls)switch(e.event){case"init":{var i;const t=null==(i=this.workerContext)?void 0:i.objectURL;t&&self.URL.revokeObjectURL(t);break}case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":A[e.data.logType]&&A[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,s.trigger(e.event,e.data)}}configureTransmuxer(t){const{transmuxer:e}=this;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)}handleTransmuxComplete(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)}}function xr(t,e){if(t.length!==e.length)return!1;for(let s=0;st[s]!==e[s]))}function Mr(t,e){return e.label.toLowerCase()===t.name.toLowerCase()&&(!e.language||e.language.toLowerCase()===(t.lang||"").toLowerCase())}class Fr{constructor(t){this.buffered=void 0;const e=(e,s,i)=>{if((s>>>=0)>i-1)throw new DOMException(`Failed to execute '${e}' on 'TimeRanges': The index provided (${s}) is greater than the maximum bound (${i})`);return t[s][e]};this.buffered={get length(){return t.length},end:s=>e("end",s,t.length),start:s=>e("start",s,t.length)}}}class Or{constructor(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}append(t,e,s){const i=this.queues[e];i.push(t),1!==i.length||s||this.executeNext(e)}insertAbort(t,e){this.queues[e].unshift(t),this.executeNext(e)}appendBlocker(t){let e;const s=new Promise((t=>{e=t})),i={execute:e,onStart:()=>{},onComplete:()=>{},onError:()=>{}};return this.append(i,t),s}executeNext(t){const e=this.queues[t];if(e.length){const s=e[0];try{s.execute()}catch(e){A.warn(`[buffer-operation-queue]: Exception executing "${t}" SourceBuffer operation: ${e}`),s.onError(e);const i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}}shiftAndExecuteNext(t){this.queues[t].shift(),this.executeNext(t)}current(t){return this.queues[t][0]}}const Nr=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/;function Ur(t){const e=t.querySelectorAll("source");[].slice.call(e).forEach((e=>{t.removeChild(e)}))}const Br={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},$r=t=>String.fromCharCode(Br[t]||t),Gr=15,Kr=100,Hr={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},Vr={17:2,18:4,21:6,22:8,23:10,19:13,20:15},Yr={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},Wr={25:2,26:4,29:6,30:8,31:10,27:13,28:15},jr=["white","green","blue","cyan","red","yellow","magenta","black","transparent"];class qr{constructor(){this.time=null,this.verboseLevel=0}log(t,e){if(this.verboseLevel>=t){const s="function"==typeof e?e():e;A.log(`${this.time} [${t}] ${s}`)}}}const Xr=function(t){const e=[];for(let s=0;sKr&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=Kr)}moveCursor(t){const e=this.pos+t;if(t>1)for(let t=this.pos+1;t=144&&this.backSpace();const e=$r(t);this.pos>=Kr?this.logger.log(0,(()=>"Cannot insert "+t.toString(16)+" ("+e+") at position "+this.pos+". Skipping it!")):(this.chars[this.pos].setChar(e,this.currPenState),this.moveCursor(1))}clearFromPos(t){let e;for(e=t;e"pacData = "+JSON.stringify(t)));let e=t.row-1;if(this.nrRollUpRows&&e"bkgData = "+JSON.stringify(t))),this.backSpace(),this.setPen(t),this.insertChar(32)}setRollUpRows(t){this.nrRollUpRows=t}rollUp(){if(null===this.nrRollUpRows)return void this.logger.log(3,"roll_up but nrRollUpRows not set yet");this.logger.log(1,(()=>this.getDisplayText()));const t=this.currRow+1-this.nrRollUpRows,e=this.rows.splice(t,1)[0];e.clear(),this.rows.splice(this.currRow,0,e),this.logger.log(2,"Rolling up")}getDisplayText(t){t=t||!1;const e=[];let s="",i=-1;for(let s=0;s0&&(s=t?"["+e.join(" | ")+"]":e.join("\n")),s}getTextAndFormat(){return this.rows}}class tn{constructor(t,e,s){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new Zr(s),this.nonDisplayedMemory=new Zr(s),this.lastOutputScreen=new Zr(s),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=s}reset(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null}getHandler(){return this.outputFilter}setHandler(t){this.outputFilter=t}setPAC(t){this.writeScreen.setPAC(t)}setBkgData(t){this.writeScreen.setBkgData(t)}setMode(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(()=>"MODE="+t)),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)}insertChars(t){for(let e=0;ee+": "+this.writeScreen.getDisplayText(!0))),"MODE_PAINT-ON"!==this.mode&&"MODE_ROLL-UP"!==this.mode||(this.logger.log(1,(()=>"DISPLAYED: "+this.displayedMemory.getDisplayText(!0))),this.outputDataUpdate())}ccRCL(){this.logger.log(2,"RCL - Resume Caption Loading"),this.setMode("MODE_POP-ON")}ccBS(){this.logger.log(2,"BS - BackSpace"),"MODE_TEXT"!==this.mode&&(this.writeScreen.backSpace(),this.writeScreen===this.displayedMemory&&this.outputDataUpdate())}ccAOF(){}ccAON(){}ccDER(){this.logger.log(2,"DER- Delete to End of Row"),this.writeScreen.clearToEndOfRow(),this.outputDataUpdate()}ccRU(t){this.logger.log(2,"RU("+t+") - Roll Up"),this.writeScreen=this.displayedMemory,this.setMode("MODE_ROLL-UP"),this.writeScreen.setRollUpRows(t)}ccFON(){this.logger.log(2,"FON - Flash On"),this.writeScreen.setPen({flash:!0})}ccRDC(){this.logger.log(2,"RDC - Resume Direct Captioning"),this.setMode("MODE_PAINT-ON")}ccTR(){this.logger.log(2,"TR"),this.setMode("MODE_TEXT")}ccRTD(){this.logger.log(2,"RTD"),this.setMode("MODE_TEXT")}ccEDM(){this.logger.log(2,"EDM - Erase Displayed Memory"),this.displayedMemory.reset(),this.outputDataUpdate(!0)}ccCR(){this.logger.log(2,"CR - Carriage Return"),this.writeScreen.rollUp(),this.outputDataUpdate(!0)}ccENM(){this.logger.log(2,"ENM - Erase Non-displayed Memory"),this.nonDisplayedMemory.reset()}ccEOC(){if(this.logger.log(2,"EOC - End Of Caption"),"MODE_POP-ON"===this.mode){const t=this.displayedMemory;this.displayedMemory=this.nonDisplayedMemory,this.nonDisplayedMemory=t,this.writeScreen=this.nonDisplayedMemory,this.logger.log(1,(()=>"DISP: "+this.displayedMemory.getDisplayText()))}this.outputDataUpdate(!0)}ccTO(t){this.logger.log(2,"TO("+t+") - Tab Offset"),this.writeScreen.moveCursor(t)}ccMIDROW(t){const e={flash:!1};if(e.underline=t%2==1,e.italics=t>=46,e.italics)e.foreground="white";else{const s=Math.floor(t/2)-16,i=["white","green","blue","cyan","red","yellow","magenta"];e.foreground=i[s]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)}outputDataUpdate(t=!1){const e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))}cueSplitAtTime(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))}}class en{constructor(t,e,s){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;const i=this.logger=new qr;this.channels=[null,new tn(t,e,i),new tn(t+1,s,i)]}getHandler(t){return this.channels[t].getHandler()}setHandler(t,e){this.channels[t].setHandler(e)}addData(t,e){this.logger.time=t;for(let t=0;t"["+Xr([e[t],e[t+1]])+"] -> ("+Xr([s,i])+")"));const a=this.cmdHistory;if(s>=16&&s<=31){if(rn(s,i,a)){sn(null,null,a),this.logger.log(3,(()=>"Repeated command ("+Xr([s,i])+") is dropped"));continue}sn(s,i,this.cmdHistory),r=this.parseCmd(s,i),r||(r=this.parseMidrow(s,i)),r||(r=this.parsePAC(s,i)),r||(r=this.parseBackgroundAttributes(s,i))}else sn(null,null,a);if(!r&&(n=this.parseChars(s,i),n)){const t=this.currentChannel;if(t&&t>0){this.channels[t].insertChars(n)}else this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||n||this.logger.log(2,(()=>"Couldn't parse cleaned data "+Xr([s,i])+" orig: "+Xr([e[t],e[t+1]])))}}parseCmd(t,e){if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47)&&!((23===t||31===t)&&e>=33&&e<=35))return!1;const s=20===t||21===t||23===t?1:2,i=this.channels[s];return 20===t||21===t||28===t||29===t?32===e?i.ccRCL():33===e?i.ccBS():34===e?i.ccAOF():35===e?i.ccAON():36===e?i.ccDER():37===e?i.ccRU(2):38===e?i.ccRU(3):39===e?i.ccRU(4):40===e?i.ccFON():41===e?i.ccRDC():42===e?i.ccTR():43===e?i.ccRTD():44===e?i.ccEDM():45===e?i.ccCR():46===e?i.ccENM():47===e&&i.ccEOC():i.ccTO(e-32),this.currentChannel=s,!0}parseMidrow(t,e){let s=0;if((17===t||25===t)&&e>=32&&e<=47){if(s=17===t?1:2,s!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;const i=this.channels[s];return!!i&&(i.ccMIDROW(e),this.logger.log(3,(()=>"MIDROW ("+Xr([t,e])+")")),!0)}return!1}parsePAC(t,e){let s;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127)&&!((16===t||24===t)&&e>=64&&e<=95))return!1;const i=t<=23?1:2;s=e>=64&&e<=95?1===i?Hr[t]:Yr[t]:1===i?Vr[t]:Wr[t];const r=this.channels[i];return!!r&&(r.setPAC(this.interpretPAC(s,e)),this.currentChannel=i,!0)}interpretPAC(t,e){let s;const i={color:null,italics:!1,indent:null,underline:!1,row:t};return s=e>95?e-96:e-64,i.underline=!(1&~s),s<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(s/2)]:s<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((s-16)/2),i}parseChars(t,e){let s,i=null,r=null;if(t>=25?(s=2,r=t-8):(s=1,r=t),r>=17&&r<=19){let t;t=17===r?e+80:18===r?e+112:e+144,this.logger.log(2,(()=>"Special char '"+$r(t)+"' in channel "+s)),i=[t]}else t>=32&&t<=127&&(i=0===e?[t]:[t,e]);return i&&this.logger.log(3,(()=>"Char codes = "+Xr(i).join(","))),i}parseBackgroundAttributes(t,e){if(!((16===t||24===t)&&e>=32&&e<=47)&&!((23===t||31===t)&&e>=45&&e<=47))return!1;let s;const i={};16===t||24===t?(s=Math.floor((e-32)/2),i.background=jr[s],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));const r=t<=23?1:2;return this.channels[r].setBkgData(i),!0}reset(){for(let t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=s,this.timelineController.createCaptionsTrack(this.trackName)}reset(){this.cueRanges=[],this.startTime=null}}var an=function(){if(null!=$&&$.VTTCue)return self.VTTCue;const t=["","lr","rl"],e=["start","middle","end","left","right"];function s(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;const s=e.toLowerCase();return!!~t.indexOf(s)&&s}function i(t){return s(e,t)}function r(t,...e){let s=1;for(;s100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",r({},l,{get:function(){return T},set:function(t){const e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",r({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",r({},l,{get:function(){return L},set:function(t){const e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return n.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},n}();class on{decode(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))}}function ln(t){function e(t,e,s,i){return 3600*(0|t)+60*(0|e)+(0|s)+parseFloat(i||0)}const s=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return s?parseFloat(s[2])>59?e(s[2],s[3],0,s[4]):e(s[1],s[2],s[3],s[4]):null}class hn{constructor(){this.values=Object.create(null)}set(t,e){this.get(t)||""===e||(this.values[t]=e)}get(t,e,s){return s?this.has(t)?this.values[t]:e[s]:this.has(t)?this.values[t]:e}has(t){return t in this.values}alt(t,e,s){for(let i=0;i=0&&s<=100)return this.set(t,s),!0}return!1}}function dn(t,e,s,i){const r=i?t.split(i):[t];for(const t in r){if("string"!=typeof r[t])continue;const i=r[t].split(s);if(2!==i.length)continue;e(i[0],i[1])}}const cn=new an(0,0,""),un="middle"===cn.align?"middle":"center";function fn(t,e,s){const i=t;function r(){const e=ln(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function n(){t=t.replace(/^\s+/,"")}if(n(),e.startTime=r(),n(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),n(),e.endTime=r(),n(),function(t,e){const i=new hn;dn(t,(function(t,e){let r;switch(t){case"region":for(let r=s.length-1;r>=0;r--)if(s[r].id===e){i.set(t,s[r].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":r=e.split(","),i.integer(t,r[0]),i.percent(t,r[0])&&i.set("snapToLines",!1),i.alt(t,r[0],["auto"]),2===r.length&&i.alt("lineAlign",r[1],["start",un,"end"]);break;case"position":r=e.split(","),i.percent(t,r[0]),2===r.length&&i.alt("positionAlign",r[1],["start",un,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",un,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");let r=i.get("line","auto");"auto"===r&&-1===cn.line&&(r=-1),e.line=r,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",un);let n=i.get("position","auto");"auto"===n&&50===cn.position&&(n="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=n}(t,e)}function gn(t){return t.replace(//gi,"\n")}class mn{constructor(){this.state="INITIAL",this.buffer="",this.decoder=new on,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}parse(t){const e=this;function s(){let t=e.buffer,s=0;for(t=gn(t);s>>0).toString()};function En(t,e,s){return yn(t.toString())+yn(e.toString())+yn(s)}function Tn(t,e,s,i,r,n,a){const o=new mn,l=vt(new Uint8Array(t)).trim().replace(pn,"\n").split("\n"),h=[],d=e?function(t,e=1){return ur(t,9e4,1/e)}(e.baseTime,e.timescale):0;let c,u="00:00.000",g=0,m=0,p=!0;o.oncue=function(t){const n=s[i];let a=s.ccOffset;const o=(g-d)/9e4;if(null!=n&&n.new&&(void 0!==m?a=s.ccOffset=n.start:function(t,e,s){let i=t[e],r=t[i.prevCC];if(!r||!r.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(n=r)&&n.new;){var n;t.ccOffset+=i.start-r.start,i.new=!1,i=r,r=t[i.prevCC]}t.presentationOffset=s}(s,i,o)),o){if(!e)return void(c=new Error("Missing initPTS for VTT MPEGTS"));a=o-s.presentationOffset}const l=t.endTime-t.startTime,u=yr(9e4*(t.startTime+a-m),9e4*r)/9e4;t.startTime=Math.max(u,0),t.endTime=Math.max(u+l,0);const f=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(f)),t.id||(t.id=En(t.startTime,t.endTime,f)),t.endTime>0&&h.push(t)},o.onparsingerror=function(t){c=t},o.onflush=function(){c?a(c):n(h)},l.forEach((t=>{if(p){if(vn(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((t=>{vn(t,"LOCAL:")?u=t.slice(6):vn(t,"MPEGTS:")&&(g=parseInt(t.slice(7)))}));try{m=function(t){let e=parseInt(t.slice(-3));const s=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),r=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(f(e)&&f(s)&&f(i)&&f(r)))throw Error(`Malformed X-TIMESTAMP-MAP: Local:${t}`);return e+=1e3*s,e+=6e4*i,e+=36e5*r,e}(u)/1e3}catch(t){c=t}return}""===t&&(p=!1)}o.parse(t+"\n")})),o.flush()}const Sn="stpp.ttml.im1t",Ln=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,An=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Rn={left:"start",center:"center",right:"end",start:"start",end:"end"};function bn(t,e,s,i){const r=_t(new Uint8Array(t),["mdat"]);if(0===r.length)return void i(new Error("Could not parse IMSC1 mdat"));const n=r.map((t=>vt(t))),a=function(t,e,s=1,i=!1){return ur(t,e,1/s,i)}(e.baseTime,1,e.timescale);try{n.forEach((t=>s(function(t,e){const s=(new DOMParser).parseFromString(t,"text/xml"),i=s.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");const r={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},n=Object.keys(r).reduce(((t,e)=>(t[e]=i.getAttribute(`ttp:${e}`)||r[e],t)),{}),a="preserve"!==i.getAttribute("xml:space"),o=wn(kn(i,"styling","style")),l=wn(kn(i,"layout","region")),h=kn(i,"body","[begin]");return[].map.call(h,(t=>{const s=Dn(t,a);if(!s||!t.hasAttribute("begin"))return null;const i=Cn(t.getAttribute("begin"),n),r=Cn(t.getAttribute("dur"),n);let h=Cn(t.getAttribute("end"),n);if(null===i)throw _n(t);if(null===h){if(null===r)throw _n(t);h=i+r}const d=new an(i-e,h-e,s);d.id=En(d.startTime,d.endTime,d.text);const c=function(t,e,s){const i="http://www.w3.org/ns/ttml#styling";let r=null;const n=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],a=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;a&&s.hasOwnProperty(a)&&(r=s[a]);return n.reduce(((s,n)=>{const a=In(e,i,n)||In(t,i,n)||In(r,i,n);return a&&(s[n]=a),s}),{})}(l[t.getAttribute("region")],o[t.getAttribute("style")],o),{textAlign:f}=c;if(f){const t=Rn[f];t&&(d.lineAlign=t),d.align=f}return u(d,c),d})).filter((t=>null!==t))}(t,a))))}catch(t){i(t)}}function kn(t,e,s){const i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(s)):[]}function wn(t){return t.reduce(((t,e)=>{const s=e.getAttribute("xml:id");return s&&(t[s]=e),t}),{})}function Dn(t,e){return[].slice.call(t.childNodes).reduce(((t,s,i)=>{var r;return"br"===s.nodeName&&i?t+"\n":null!=(r=s.childNodes)&&r.length?Dn(s,e):e?t+s.textContent.trim().replace(/\s+/g," "):t+s.textContent}),"")}function In(t,e,s){return t&&t.hasAttributeNS(e,s)?t.getAttributeNS(e,s):null}function _n(t){return new Error(`Could not parse ttml timestamp ${t}`)}function Cn(t,e){if(!t)return null;let s=ln(t);return null===s&&(Ln.test(t)?s=function(t,e){const s=Ln.exec(t),i=(0|s[4])+(0|s[5])/e.subFrameRate;return 3600*(0|s[1])+60*(0|s[2])+(0|s[3])+i/e.frameRate}(t,e):An.test(t)&&(s=function(t,e){const s=An.exec(t),i=Number(s[1]);switch(s[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),s}function xn(t){return t.characteristics&&/transcribes-spoken-dialog/gi.test(t.characteristics)&&/describes-music-and-sound/gi.test(t.characteristics)?"captions":"subtitles"}function Pn(t,e){return!!t&&t.kind===xn(e)&&Mr(e,t)}class Mn{constructor(t){this.hls=void 0,this.autoLevelCapping=void 0,this.firstLevel=void 0,this.media=void 0,this.restrictedLevels=void 0,this.timer=void 0,this.clientRect=void 0,this.streamController=void 0,this.hls=t,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.firstLevel=-1,this.media=null,this.restrictedLevels=[],this.timer=void 0,this.clientRect=null,this.registerListeners()}setStreamController(t){this.streamController=t}destroy(){this.hls&&this.unregisterListener(),this.timer&&this.stopCapping(),this.media=null,this.clientRect=null,this.hls=this.streamController=null}registerListeners(){const{hls:t}=this;t.on(p.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.BUFFER_CODECS,this.onBufferCodecs,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this)}unregisterListener(){const{hls:t}=this;t.off(p.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.BUFFER_CODECS,this.onBufferCodecs,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this)}onFpsDropLevelCapping(t,e){const s=this.hls.levels[e.droppedLevel];this.isLevelAllowed(s)&&this.restrictedLevels.push({bitrate:s.bitrate,height:s.height,width:s.width})}onMediaAttaching(t,e){this.media=e.media instanceof HTMLVideoElement?e.media:null,this.clientRect=null,this.timer&&this.hls.levels.length&&this.detectPlayerSize()}onManifestParsed(t,e){const s=this.hls;this.restrictedLevels=[],this.firstLevel=e.firstLevel,s.config.capLevelToPlayerSize&&e.video&&this.startCapping()}onLevelsUpdated(t,e){this.timer&&f(this.autoLevelCapping)&&this.detectPlayerSize()}onBufferCodecs(t,e){this.hls.config.capLevelToPlayerSize&&e.video&&this.startCapping()}onMediaDetaching(){this.stopCapping()}detectPlayerSize(){if(this.media){if(this.mediaHeight<=0||this.mediaWidth<=0)return void(this.clientRect=null);const t=this.hls.levels;if(t.length){const e=this.hls,s=this.getMaxLevel(t.length-1);s!==this.autoLevelCapping&&A.log(`Setting autoLevelCapping to ${s}: ${t[s].height}p@${t[s].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`),e.autoLevelCapping=s,e.autoLevelCapping>this.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}}getMaxLevel(t){const e=this.hls.levels;if(!e.length)return-1;const s=e.filter(((e,s)=>this.isLevelAllowed(e)&&s<=t));return this.clientRect=null,Mn.getMaxLevelByMediaSize(s,this.mediaWidth,this.mediaHeight)}startCapping(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())}stopCapping(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)}getDimensions(){if(this.clientRect)return this.clientRect;const t=this.media,e={width:0,height:0};if(t){const s=t.getBoundingClientRect();e.width=s.width,e.height=s.height,e.width||e.height||(e.width=s.right-s.left||t.width||0,e.height=s.bottom-s.top||t.height||0)}return this.clientRect=e,e}get mediaWidth(){return this.getDimensions().width*this.contentScaleFactor}get mediaHeight(){return this.getDimensions().height*this.contentScaleFactor}get contentScaleFactor(){let t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}isLevelAllowed(t){return!this.restrictedLevels.some((e=>t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height))}static getMaxLevelByMediaSize(t,e,s){if(null==t||!t.length)return-1;let i=t.length-1;const r=Math.max(e,s);for(let e=0;e=r||s.height>=r)&&(n=s,!(a=t[e+1])||n.width!==a.width||n.height!==a.height)){i=e;break}}var n,a;return i}}const Fn="[eme]";class On{constructor(t){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=On.CDMCleanupPromise?[On.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=A.debug.bind(A,Fn),this.log=A.log.bind(A,Fn),this.warn=A.warn.bind(A,Fn),this.error=A.error.bind(A,Fn),this.hls=t,this.config=t.config,this.registerListeners()}destroy(){this.unregisterListeners(),this.onMediaDetached();const t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null}registerListeners(){this.hls.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(p.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(p.MANIFEST_LOADED,this.onManifestLoaded,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(p.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(p.MANIFEST_LOADED,this.onManifestLoaded,this)}getLicenseServerUrl(t){const{drmSystems:e,widevineLicenseUrl:s}=this.config,i=e[t];if(i)return i.licenseUrl;if(t===G.WIDEVINE&&s)return s;throw new Error(`no license server URL configured for key-system "${t}"`)}getServerCertificateUrl(t){const{drmSystems:e}=this.config,s=e[t];if(s)return s.serverCertificateUrl;this.log(`No Server Certificate in config.drmSystems["${t}"]`)}attemptKeySystemAccess(t){const e=this.hls.levels,s=(t,e,s)=>!!t&&s.indexOf(t)===e,i=e.map((t=>t.audioCodec)).filter(s),r=e.map((t=>t.videoCodec)).filter(s);return i.length+r.length===0&&r.push("avc1.42e01e"),new Promise(((e,s)=>{const n=t=>{const a=t.shift();this.getMediaKeysPromise(a,i,r).then((t=>e({keySystem:a,mediaKeys:t}))).catch((e=>{t.length?n(t):s(e instanceof Nn?e:new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))};n(t)}))}requestMediaKeySystemAccess(t,e){const{requestMediaKeySystemAccessFunc:s}=this.config;if("function"!=typeof s){let t=`Configured requestMediaKeySystemAccess is not a function ${s}`;return null===tt&&"http:"===self.location.protocol&&(t=`navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`),Promise.reject(new Error(t))}return s(t,e)}getMediaKeysPromise(t,e,s){const i=function(t,e,s,i){let r;switch(t){case G.FAIRPLAY:r=["cenc","sinf"];break;case G.WIDEVINE:case G.PLAYREADY:r=["cenc"];break;case G.CLEARKEY:r=["cenc","keyids"];break;default:throw new Error(`Unknown key-system: ${t}`)}return function(t,e,s,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((t=>({contentType:`audio/mp4; codecs="${t}"`,robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}))),videoCapabilities:s.map((t=>({contentType:`video/mp4; codecs="${t}"`,robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null})))}]}(r,e,s,i)}(t,e,s,this.config.drmSystemOptions),r=this.keySystemAccessPromises[t];let n=null==r?void 0:r.keySystemAccess;if(!n){this.log(`Requesting encrypted media "${t}" key-system access with config: ${JSON.stringify(i)}`),n=this.requestMediaKeySystemAccess(t,i);const e=this.keySystemAccessPromises[t]={keySystemAccess:n};return n.catch((e=>{this.log(`Failed to obtain access to key-system "${t}": ${e}`)})),n.then((s=>{this.log(`Access for key-system "${s.keySystem}" obtained`);const i=this.fetchServerCertificate(t);return this.log(`Create media-keys for "${t}"`),e.mediaKeys=s.createMediaKeys().then((e=>(this.log(`Media-keys created for "${t}"`),i.then((s=>s?this.setMediaKeysServerCertificate(e,t,s):e))))),e.mediaKeys.catch((e=>{this.error(`Failed to create media-keys for "${t}"}: ${e}`)})),e.mediaKeys}))}return n.then((()=>r.mediaKeys))}createMediaKeySessionContext({decryptdata:t,keySystem:e,mediaKeys:s}){this.log(`Creating key-system session "${e}" keyId: ${Tt(t.keyId||[])}`);const i=s.createSession(),r={decryptdata:t,keySystem:e,mediaKeys:s,mediaKeysSession:i,keyStatus:"status-pending"};return this.mediaKeySessions.push(r),r}renewKeySession(t){const e=t.decryptdata;if(e.pssh){const s=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e),r="cenc";this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(s,r,e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)}getKeyIdString(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Tt(t.keyId)}updateKeySession(t,e){var s;const i=t.mediaKeysSession;return this.log(`Updating key-session "${i.sessionId}" for keyID ${Tt((null==(s=t.decryptdata)?void 0:s.keyId)||[])}\n } (data length: ${e?e.byteLength:e})`),i.update(e)}selectKeySystemFormat(t){const e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log(`Selecting key-system from fragment (sn: ${t.sn} ${t.type}: ${t.level}) key formats ${e.join(", ")}`),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise}getKeyFormatPromise(t){return new Promise(((e,s)=>{const i=Z(this.config),r=t.map(W).filter((t=>!!t&&-1!==i.indexOf(t)));return this.getKeySystemSelectionPromise(r).then((({keySystem:t})=>{const i=J(t);i?e(i):s(new Error(`Unable to find format for key-system "${t}"`))})).catch(s)}))}loadKey(t){const e=t.keyInfo.decryptdata,s=this.getKeyIdString(e),i=`(keyId: ${s} format: "${e.keyFormat}" method: ${e.method} uri: ${e.uri})`;this.log(`Starting session for key ${i}`);let r=this.keyIdToKeySessionPromise[s];return r||(r=this.keyIdToKeySessionPromise[s]=this.getKeySystemForKeyPromise(e).then((({keySystem:s,mediaKeys:r})=>(this.throwIfDestroyed(),this.log(`Handle encrypted media sn: ${t.frag.sn} ${t.frag.type}: ${t.frag.level} using key ${i}`),this.attemptSetMediaKeys(s,r).then((()=>{this.throwIfDestroyed();const t=this.createMediaKeySessionContext({keySystem:s,mediaKeys:r,decryptdata:e});return this.generateRequestWithPreferredKeySession(t,"cenc",e.pssh,"playlist-key")}))))),r.catch((t=>this.handleError(t)))),r}throwIfDestroyed(t="Invalid state"){if(!this.hls)throw new Error("invalid state")}handleError(t){this.hls&&(this.error(t.message),t instanceof Nn?this.hls.trigger(p.ERROR,t.data):this.hls.trigger(p.ERROR,{type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))}getKeySystemForKeyPromise(t){const e=this.getKeyIdString(t),s=this.keyIdToKeySessionPromise[e];if(!s){const e=W(t.keyFormat),s=e?[e]:Z(this.config);return this.attemptKeySystemAccess(s)}return s}getKeySystemSelectionPromise(t){if(t.length||(t=Z(this.config)),0===t.length)throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},`Missing key-system license configuration options ${JSON.stringify({drmSystems:this.config.drmSystems})}`);return this.attemptKeySystemAccess(t)}_onMediaEncrypted(t){const{initDataType:e,initData:s}=t,i=`"${t.type}" event: init data type: "${e}"`;if(this.debug(i),null===s)return;let r,n;if("sinf"===e&&this.config.drmSystems[G.FAIRPLAY]){const t=Rt(new Uint8Array(s));try{const e=N(JSON.parse(t).sinf),s=Nt(new Uint8Array(e));if(!s)throw new Error("'schm' box missing or not cbcs/cenc with schi > tenc");r=s.subarray(8,24),n=G.FAIRPLAY}catch(t){return void this.warn(`${i} Failed to parse sinf: ${t}`)}}else{const t=function(t){const e=[];if(t instanceof ArrayBuffer){const s=t.byteLength;let i=0;for(;i+32t.systemId===z))[0];if(!e)return void(0===t.length||t.some((t=>!t.systemId))?this.warn(`${i} contains incomplete or invalid pssh data`):this.log(`ignoring ${i} for ${t.map((t=>Q(t.systemId))).join(",")} pssh data in favor of playlist keys`));if(n=Q(e.systemId),0===e.version&&e.data){const t=e.data.length-22;r=e.data.subarray(t,t+16)}}if(!n||!r)return;const a=Tt(r),{keyIdToKeySessionPromise:o,mediaKeySessions:l}=this;let h=o[a];for(let t=0;tthis.generateRequestWithPreferredKeySession(i,e,s,"encrypted-event-key-match")));break}}h||(h=o[a]=this.getKeySystemSelectionPromise([n]).then((({keySystem:t,mediaKeys:i})=>{var n;this.throwIfDestroyed();const o=new jt("ISO-23001-7",a,null!=(n=J(t))?n:"");return o.pssh=new Uint8Array(s),o.keyId=r,this.attemptSetMediaKeys(t,i).then((()=>{this.throwIfDestroyed();const r=this.createMediaKeySessionContext({decryptdata:o,keySystem:t,mediaKeys:i});return this.generateRequestWithPreferredKeySession(r,e,s,"encrypted-event-no-match")}))}))),h.catch((t=>this.handleError(t)))}_onWaitingForKey(t){this.log(`"${t.type}" event`)}attemptSetMediaKeys(t,e){const s=this.setMediaKeysQueue.slice();this.log(`Setting media-keys for "${t}"`);const i=Promise.all(s).then((()=>{if(!this.media)throw new Error("Attempted to set mediaKeys without media element attached");return this.media.setMediaKeys(e)}));return this.setMediaKeysQueue.push(i),i.then((()=>{this.log(`Media-keys set for "${t}"`),s.push(i),this.setMediaKeysQueue=this.setMediaKeysQueue.filter((t=>-1===s.indexOf(t)))}))}generateRequestWithPreferredKeySession(t,e,s,i){var r,n;const a=null==(r=this.config.drmSystems)||null==(n=r[t.keySystem])?void 0:n.generateRequest;if(a)try{const i=a.call(this.hls,e,s,t);if(!i)throw new Error("Invalid response from configured generateRequest filter");e=i.initDataType,s=t.decryptdata.pssh=i.initData?new Uint8Array(i.initData):null}catch(t){var o;if(this.warn(t.message),null!=(o=this.hls)&&o.config.debug)throw t}if(null===s)return this.log(`Skipping key-session request for "${i}" (no initData)`),Promise.resolve(t);const l=this.getKeyIdString(t.decryptdata);this.log(`Generating key-session request for "${i}": ${l} (init data type: ${e} length: ${s?s.byteLength:null})`);const h=new _r,d=t._onmessage=e=>{const s=t.mediaKeysSession;if(!s)return void h.emit("error",new Error("invalid state"));const{messageType:i,message:r}=e;this.log(`"${i}" message event for session "${s.sessionId}" message size: ${r.byteLength}`),"license-request"===i||"license-renewal"===i?this.renewLicense(t,r).catch((t=>{this.handleError(t),h.emit("error",t)})):"license-release"===i?t.keySystem===G.FAIRPLAY&&(this.updateKeySession(t,B("acknowledged")),this.removeSession(t)):this.warn(`unhandled media key message type "${i}"`)},c=t._onkeystatuseschange=e=>{if(!t.mediaKeysSession)return void h.emit("error",new Error("invalid state"));this.onKeyStatusChange(t);const s=t.keyStatus;h.emit("keyStatus",s),"expired"===s&&(this.warn(`${t.keySystem} expired for key ${l}`),this.renewKeySession(t))};t.mediaKeysSession.addEventListener("message",d),t.mediaKeysSession.addEventListener("keystatuseschange",c);const u=new Promise(((t,e)=>{h.on("error",e),h.on("keyStatus",(s=>{s.startsWith("usable")?t():"output-restricted"===s?e(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED,fatal:!1},"HDCP level output restricted")):"internal-error"===s?e(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_STATUS_INTERNAL_ERROR,fatal:!0},`key status changed to "${s}"`)):"expired"===s?e(new Error("key expired while generating request")):this.warn(`unhandled key status change "${s}"`)}))}));return t.mediaKeysSession.generateRequest(e,s).then((()=>{var e;this.log(`Request generated for key-session "${null==(e=t.mediaKeysSession)?void 0:e.sessionId}" keyId: ${l}`)})).catch((t=>{throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_SESSION,error:t,fatal:!1},`Error generating key-session request: ${t}`)})).then((()=>u)).catch((e=>{throw h.removeAllListeners(),this.removeSession(t),e})).then((()=>(h.removeAllListeners(),t)))}onKeyStatusChange(t){t.mediaKeysSession.keyStatuses.forEach(((e,s)=>{this.log(`key status change "${e}" for keyStatuses keyId: ${Tt("buffer"in s?new Uint8Array(s.buffer,s.byteOffset,s.byteLength):new Uint8Array(s))} session keyId: ${Tt(new Uint8Array(t.decryptdata.keyId||[]))} uri: ${t.decryptdata.uri}`),t.keyStatus=e}))}fetchServerCertificate(t){const e=this.config,s=new(0,e.loader)(e),i=this.getServerCertificateUrl(t);return i?(this.log(`Fetching server certificate for "${t}"`),new Promise(((r,n)=>{const a={responseType:"arraybuffer",url:i},o=e.certLoadPolicy.default,l={loadPolicy:o,timeout:o.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0},d={onSuccess:(t,e,s,i)=>{r(t.data)},onError:(e,s,r,o)=>{n(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,fatal:!0,networkDetails:r,response:h({url:a.url,data:void 0},e)},`"${t}" certificate request failed (${i}). Status: ${e.code} (${e.text})`))},onTimeout:(e,s,r)=>{n(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,fatal:!0,networkDetails:r,response:{url:a.url,data:void 0}},`"${t}" certificate request timed out (${i})`))},onAbort:(t,e,s)=>{n(new Error("aborted"))}};s.load(a,l,d)}))):Promise.resolve()}setMediaKeysServerCertificate(t,e,s){return new Promise(((i,r)=>{t.setServerCertificate(s).then((r=>{this.log(`setServerCertificate ${r?"success":"not supported by CDM"} (${null==s?void 0:s.byteLength}) on "${e}"`),i(t)})).catch((t=>{r(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,error:t,fatal:!0},t.message))}))}))}renewLicense(t,e){return this.requestLicense(t,new Uint8Array(e)).then((e=>this.updateKeySession(t,new Uint8Array(e)).catch((t=>{throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SESSION_UPDATE_FAILED,error:t,fatal:!0},t.message)}))))}unpackPlayReadyKeyMessage(t,e){const s=String.fromCharCode.apply(null,new Uint16Array(e.buffer));if(!s.includes("PlayReadyKeyMessage"))return t.setRequestHeader("Content-Type","text/xml; charset=utf-8"),e;const i=(new DOMParser).parseFromString(s,"application/xml"),r=i.querySelectorAll("HttpHeader");if(r.length>0){let e;for(let s=0,i=r.length;s in key message");return B(atob(l))}setupLicenseXHR(t,e,s,i){const r=this.config.licenseXhrSetup;return r?Promise.resolve().then((()=>{if(!s.decryptdata)throw new Error("Key removed");return r.call(this.hls,t,e,s,i)})).catch((n=>{if(!s.decryptdata)throw n;return t.open("POST",e,!0),r.call(this.hls,t,e,s,i)})).then((s=>{t.readyState||t.open("POST",e,!0);return{xhr:t,licenseChallenge:s||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))}requestLicense(t,e){const s=this.config.keyLoadPolicy.default;return new Promise(((i,r)=>{const n=this.getLicenseServerUrl(t.keySystem);this.log(`Sending license request to URL: ${n}`);const a=new XMLHttpRequest;a.responseType="arraybuffer",a.onreadystatechange=()=>{if(!this.hls||!t.mediaKeysSession)return r(new Error("invalid state"));if(4===a.readyState)if(200===a.status){this._requestLicenseFailureCount=0;let e=a.response;this.log(`License received ${e instanceof ArrayBuffer?e.byteLength:e}`);const s=this.config.licenseResponseCallback;if(s)try{e=s.call(this.hls,a,n,t)}catch(t){this.error(t)}i(e)}else{const o=s.errorRetry,l=o?o.maxNumRetry:0;if(this._requestLicenseFailureCount++,this._requestLicenseFailureCount>l||a.status>=400&&a.status<500)r(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:a,response:{url:n,data:void 0,code:a.status,text:a.statusText}},`License Request XHR failed (${n}). Status: ${a.status} (${a.statusText})`));else{const s=l-this._requestLicenseFailureCount+1;this.warn(`Retrying license request, ${s} attempts left`),this.requestLicense(t,e).then(i,r)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=a,this.setupLicenseXHR(a,n,t,e).then((({xhr:e,licenseChallenge:s})=>{t.keySystem==G.PLAYREADY&&(s=this.unpackPlayReadyKeyMessage(e,s)),e.send(s)}))}))}onMediaAttached(t,e){if(!this.config.emeEnabled)return;const s=e.media;this.media=s,s.addEventListener("encrypted",this.onMediaEncrypted),s.addEventListener("waitingforkey",this.onWaitingForKey)}onMediaDetached(){const t=this.media,e=this.mediaKeySessions;t&&(t.removeEventListener("encrypted",this.onMediaEncrypted),t.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},jt.clearKeyUriToKeyIdMap();const s=e.length;On.CDMCleanupPromise=Promise.all(e.map((t=>this.removeSession(t))).concat(null==t?void 0:t.setMediaKeys(null).catch((t=>{this.log(`Could not clear media keys: ${t}`)})))).then((()=>{s&&(this.log("finished closing key sessions and clearing media keys"),e.length=0)})).catch((t=>{this.log(`Could not close sessions and clear media keys: ${t}`)}))}onManifestLoading(){this.keyFormatPromise=null}onManifestLoaded(t,{sessionKeys:e}){if(e&&this.config.emeEnabled&&!this.keyFormatPromise){const t=e.reduce(((t,e)=>(-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t)),[]);this.log(`Selecting key-system from session-keys ${t.join(", ")}`),this.keyFormatPromise=this.getKeyFormatPromise(t)}}removeSession(t){const{mediaKeysSession:e,licenseXhr:s}=t;if(e){this.log(`Remove licenses and keys and close session ${e.sessionId}`),t._onmessage&&(e.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(e.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),s&&s.readyState!==XMLHttpRequest.DONE&&s.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;const i=this.mediaKeySessions.indexOf(t);return i>-1&&this.mediaKeySessions.splice(i,1),e.remove().catch((t=>{this.log(`Could not remove session: ${t}`)})).then((()=>e.close())).catch((t=>{this.log(`Could not close session: ${t}`)}))}}}On.CDMCleanupPromise=void 0;class Nn extends Error{constructor(t,e){super(e),this.data=void 0,t.error||(t.error=new Error(e)),this.data=t,t.err=t.error}}var Un,Bn,$n;!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(Un||(Un={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}(Bn||(Bn={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}($n||($n={}));const Gn={[$n.OBJECT]:["br","d","ot","tb"],[$n.REQUEST]:["bl","dl","mtp","nor","nrr","su"],[$n.SESSION]:["cid","pr","sf","sid","st","v"],[$n.STATUS]:["bs","rtp"]};class Kn{constructor(t,e){this.value=void 0,this.params=void 0,Array.isArray(t)&&(t=t.map((t=>t instanceof Kn?t:new Kn(t)))),this.value=t,this.params=e}}class Hn{constructor(t){this.description=void 0,this.description=t}}function Vn(t,e,s,i){return new Error(`failed to ${t} "${r=e,Array.isArray(r)?JSON.stringify(r):r instanceof Map?"Map{}":r instanceof Set?"Set{}":"object"==typeof r?JSON.stringify(r):String(r)}" as ${s}`,{cause:i});var r}const Yn="Bare Item";const Wn=/[\x00-\x1f\x7f]+/;function jn(t,e,s){return Vn("serialize",t,e,s)}function qn(t){if(!1===ArrayBuffer.isView(t))throw jn(t,"Byte Sequence");return`:${e=t,btoa(String.fromCharCode(...e))}:`;var e}function Xn(t){if(function(t){return t<-999999999999999||99999999999999912)throw jn(t,"Decimal");const s=e.toString();return s.includes(".")?s:`${s}.0`}function Jn(t){const e=(s=t).description||s.toString().slice(7,-1);var s;if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(e))throw jn(e,"Token");return e}function Zn(t){switch(typeof t){case"number":if(!f(t))throw jn(t,Yn);return Number.isInteger(t)?Xn(t):Qn(t);case"string":return function(t){if(Wn.test(t))throw jn(t,"String");return`"${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}(t);case"symbol":return Jn(t);case"boolean":return function(t){if("boolean"!=typeof t)throw jn(t,"Boolean");return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return`@${Xn(t.getTime()/1e3)}`}(t);if(t instanceof Uint8Array)return qn(t);if(t instanceof Hn)return Jn(t);default:throw jn(t,Yn)}}function ta(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw jn(t,"Key");return t}function ea(t){return null==t?"":Object.entries(t).map((([t,e])=>!0===e?`;${ta(t)}`:`;${ta(t)}=${Zn(e)}`)).join("")}function sa(t){return t instanceof Kn?`${Zn(t.value)}${ea(t.params)}`:Zn(t)}function ia(t,e={whitespace:!0}){if("object"!=typeof t)throw jn(t,"Dict");const s=t instanceof Map?t.entries():Object.entries(t),i=null!=e&&e.whitespace?" ":"";return Array.from(s).map((([t,e])=>{e instanceof Kn==!1&&(e=new Kn(e));let s=ta(t);var i;return!0===e.value?s+=ea(e.params):(s+="=",Array.isArray(e.value)?s+=`(${(i=e).value.map(sa).join(" ")})${ea(i.params)}`:s+=sa(e)),s})).join(`,${i}`)}const ra=t=>Math.round(t),na=t=>100*ra(t/100),aa={br:ra,d:ra,bl:na,dl:na,mtp:na,nor:(t,e)=>(null!=e&&e.baseUrl&&(t=function(t,e){const s=new URL(t),i=new URL(e);if(s.origin!==i.origin)return t;const r=s.pathname.split("/").slice(1),n=i.pathname.split("/").slice(1,-1);for(;r[0]===n[0];)r.shift(),n.shift();for(;n.length;)n.shift(),r.unshift("..");return r.join("/")}(t,e.baseUrl)),encodeURIComponent(t)),rtp:na,tb:ra};function oa(t,e){const s={};if(null==t||"object"!=typeof t)return s;const i=Object.keys(t).sort(),r=u({},aa,null==e?void 0:e.formatters),n=null==e?void 0:e.filter;return i.forEach((i=>{if(null!=n&&n(i))return;let a=t[i];const o=r[i];o&&(a=o(a,e)),"v"===i&&1===a||"pr"==i&&1===a||(t=>"number"==typeof t?f(t):null!=t&&""!==t&&!1!==t)(a)&&((t=>"ot"===t||"sf"===t||"st"===t)(i)&&"string"==typeof a&&(a=new Hn(a)),s[i]=a)})),s}function la(t,e={}){return t?function(t,e){return ia(t,e)}(oa(t,e),u({whitespace:!1},e)):""}function ha(t,e,s){return u(t,function(t,e={}){if(!t)return{};const s=Object.entries(t),i=Object.entries(Gn).concat(Object.entries((null==e?void 0:e.customHeaderMap)||{})),r=s.reduce(((t,e)=>{var s;const[r,n]=e,a=(null==(s=i.find((t=>t[1].includes(r))))?void 0:s[0])||$n.REQUEST;return null!=t[a]||(t[a]={}),t[a][r]=n,t}),{});return Object.entries(r).reduce(((t,[s,i])=>(t[s]=la(i,e),t)),{})}(e,s))}const da=/CMCD=[^&#]+/;function ca(t,e,s){const i=function(t,e={}){if(!t)return"";const s=la(t,e);return`CMCD=${encodeURIComponent(s)}`}(e,s);if(!i)return t;if(da.test(t))return t.replace(da,i);const r=t.includes("?")?"&":"?";return`${t}${r}${i}`}function ua(t,e,s,i){t&&Object.keys(e).forEach((r=>{const n=t.filter((t=>t.groupId===r)).map((t=>{const n=u({},t);return n.details=void 0,n.attrs=new k(n.attrs),n.url=n.attrs.URI=fa(t.url,t.attrs["STABLE-RENDITION-ID"],"PER-RENDITION-URIS",s),n.groupId=n.attrs["GROUP-ID"]=e[r],n.attrs["PATHWAY-ID"]=i,n}));t.push(...n)}))}function fa(t,e,s,i){const{HOST:r,PARAMS:n,[s]:a}=i;let o;e&&(o=null==a?void 0:a[e],o&&(t=o));const l=new self.URL(t);return r&&!o&&(l.host=r),n&&Object.keys(n).sort().forEach((t=>{t&&l.searchParams.set(t,n[t])})),l.href}const ga=/^age:\s*[\d.]+\s*$/im;class ma{constructor(t){this.xhrSetup=void 0,this.requestTimeout=void 0,this.retryTimeout=void 0,this.retryDelay=void 0,this.config=null,this.callbacks=null,this.context=null,this.loader=null,this.stats=void 0,this.xhrSetup=t&&t.xhrSetup||null,this.stats=new I,this.retryDelay=0}destroy(){this.callbacks=null,this.abortInternal(),this.loader=null,this.config=null,this.context=null,this.xhrSetup=null}abortInternal(){const t=this.loader;self.clearTimeout(this.requestTimeout),self.clearTimeout(this.retryTimeout),t&&(t.onreadystatechange=null,t.onprogress=null,4!==t.readyState&&(this.stats.aborted=!0,t.abort()))}abort(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.loader)}load(t,e,s){if(this.stats.loading.start)throw new Error("Loader can only be used once.");this.stats.loading.start=self.performance.now(),this.context=t,this.config=e,this.callbacks=s,this.loadInternal()}loadInternal(){const{config:t,context:e}=this;if(!t||!e)return;const s=this.loader=new self.XMLHttpRequest,i=this.stats;i.loading.first=0,i.loaded=0,i.aborted=!1;const r=this.xhrSetup;r?Promise.resolve().then((()=>{if(this.loader===s&&!this.stats.aborted)return r(s,e.url)})).catch((t=>{if(this.loader===s&&!this.stats.aborted)return s.open("GET",e.url,!0),r(s,e.url)})).then((()=>{this.loader!==s||this.stats.aborted||this.openAndSendXhr(s,e,t)})).catch((t=>{this.callbacks.onError({code:s.status,text:t.message},e,s,i)})):this.openAndSendXhr(s,e,t)}openAndSendXhr(t,e,s){t.readyState||t.open("GET",e.url,!0);const i=e.headers,{maxTimeToFirstByteMs:r,maxLoadTimeMs:n}=s.loadPolicy;if(i)for(const e in i)t.setRequestHeader(e,i[e]);e.rangeEnd&&t.setRequestHeader("Range","bytes="+e.rangeStart+"-"+(e.rangeEnd-1)),t.onreadystatechange=this.readystatechange.bind(this),t.onprogress=this.loadprogress.bind(this),t.responseType=e.responseType,self.clearTimeout(this.requestTimeout),s.timeout=r&&f(r)?r:n,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),s.timeout),t.send()}readystatechange(){const{context:t,loader:e,stats:s}=this;if(!t||!e)return;const i=e.readyState,r=this.config;if(!s.aborted&&i>=2&&(0===s.loading.first&&(s.loading.first=Math.max(self.performance.now(),s.loading.start),r.timeout!==r.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),r.timeout=r.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),r.loadPolicy.maxLoadTimeMs-(s.loading.first-s.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;const i=e.status,n="text"!==e.responseType;if(i>=200&&i<300&&(n&&e.response||null!==e.responseText)){s.loading.end=Math.max(self.performance.now(),s.loading.first);const r=n?e.response:e.responseText,a="arraybuffer"===e.responseType?r.byteLength:r.length;if(s.loaded=s.total=a,s.bwEstimate=8e3*s.total/(s.loading.end-s.loading.first),!this.callbacks)return;const o=this.callbacks.onProgress;if(o&&o(s,t,r,e),!this.callbacks)return;const l={url:e.responseURL,data:r,code:i};this.callbacks.onSuccess(l,s,t,e)}else{const n=r.loadPolicy.errorRetry;ms(n,s.retry,!1,{url:t.url,data:void 0,code:i})?this.retry(n):(A.error(`${i} while loading ${t.url}`),this.callbacks.onError({code:i,text:e.statusText},t,e,s))}}}loadtimeout(){if(!this.config)return;const t=this.config.loadPolicy.timeoutRetry;if(ms(t,this.stats.retry,!0))this.retry(t);else{var e;A.warn(`timeout while loading ${null==(e=this.context)?void 0:e.url}`);const t=this.callbacks;t&&(this.abortInternal(),t.onTimeout(this.stats,this.context,this.loader))}}retry(t){const{context:e,stats:s}=this;this.retryDelay=fs(t,s.retry),s.retry++,A.warn(`${status?"HTTP Status "+status:"Timeout"} while loading ${null==e?void 0:e.url}, retrying ${s.retry}/${t.maxNumRetry} in ${this.retryDelay}ms`),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)}loadprogress(t){const e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)}getCacheAge(){let t=null;if(this.loader&&ga.test(this.loader.getAllResponseHeaders())){const e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t}getResponseHeader(t){return this.loader&&new RegExp(`^${t}:\\s*[\\d.]+\\s*$`,"im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null}}const pa=/(\d+)-(\d+)\/(\d+)/;class va{constructor(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||ya,this.controller=new self.AbortController,this.stats=new I}destroy(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null}abortInternal(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())}abort(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)}load(t,e,s){const i=this.stats;if(i.loading.start)throw new Error("Loader can only be used once.");i.loading.start=self.performance.now();const r=function(t,e){const s={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(u({},t.headers))};t.rangeEnd&&s.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1));return s}(t,this.controller.signal),n=s.onProgress,a="arraybuffer"===t.responseType,o=a?"byteLength":"length",{maxTimeToFirstByteMs:l,maxLoadTimeMs:h}=e.loadPolicy;this.context=t,this.config=e,this.callbacks=s,this.request=this.fetchSetup(t,r),self.clearTimeout(this.requestTimeout),e.timeout=l&&f(l)?l:h,this.requestTimeout=self.setTimeout((()=>{this.abortInternal(),s.onTimeout(i,t,this.response)}),e.timeout),self.fetch(this.request).then((r=>{this.response=this.loader=r;const o=Math.max(self.performance.now(),i.loading.start);if(self.clearTimeout(this.requestTimeout),e.timeout=h,this.requestTimeout=self.setTimeout((()=>{this.abortInternal(),s.onTimeout(i,t,this.response)}),h-(o-i.loading.start)),!r.ok){const{status:t,statusText:e}=r;throw new Ea(e||"fetch, bad network response",t,r)}return i.loading.first=o,i.total=function(t){const e=t.get("Content-Range");if(e){const t=function(t){const e=pa.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(f(t))return t}const s=t.get("Content-Length");if(s)return parseInt(s)}(r.headers)||i.total,n&&f(e.highWaterMark)?this.loadProgressively(r,i,t,e.highWaterMark,n):a?r.arrayBuffer():"json"===t.responseType?r.json():r.text()})).then((r=>{const a=this.response;if(!a)throw new Error("loader destroyed");self.clearTimeout(this.requestTimeout),i.loading.end=Math.max(self.performance.now(),i.loading.first);const l=r[o];l&&(i.loaded=i.total=l);const h={url:a.url,data:r,code:a.status};n&&!f(e.highWaterMark)&&n(i,t,r,a),s.onSuccess(h,i,t,a)})).catch((e=>{if(self.clearTimeout(this.requestTimeout),i.aborted)return;const r=e&&e.code||0,n=e?e.message:null;s.onError({code:r,text:n},t,e?e.details:null,i)}))}getCacheAge(){let t=null;if(this.response){const e=this.response.headers.get("age");t=e?parseFloat(e):null}return t}getResponseHeader(t){return this.response?this.response.headers.get(t):null}loadProgressively(t,e,s,i=0,r){const n=new bi,a=t.body.getReader(),o=()=>a.read().then((a=>{if(a.done)return n.dataLength&&r(e,s,n.flush(),t),Promise.resolve(new ArrayBuffer(0));const l=a.value,h=l.length;return e.loaded+=h,h=i&&r(e,s,n.flush(),t)):r(e,s,l,t),o()})).catch((()=>Promise.reject()));return o()}}function ya(t,e){return new self.Request(t.url,e)}class Ea extends Error{constructor(t,e,s){super(t),this.code=void 0,this.details=void 0,this.code=e,this.details=s}}const Ta=/\s/,Sa={newCue(t,e,s,i){const r=[];let n,a,o,l,h;const d=self.VTTCue||self.TextTrackCue;for(let u=0;u=16?l--:l++;const i=gn(h.trim()),f=En(e,s,i);null!=t&&null!=(c=t.cues)&&c.getCueById(f)||(a=new d(e,s,i),a.id=f,a.line=u+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*l/32)),r.push(a))}return t&&r.length&&(r.sort(((t,e)=>"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line)),r.forEach((e=>Fe(t,e)))),r}},La=h(h({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:ma,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:class{constructor(t){this.hls=void 0,this.lastLevelLoadSec=0,this.lastLoadedFragLevel=-1,this.firstSelection=-1,this._nextAutoLevel=-1,this.nextAutoLevelKey="",this.audioTracksByGroup=null,this.codecTiers=null,this.timer=-1,this.fragCurrent=null,this.partCurrent=null,this.bitrateTestDelay=0,this.bwEstimator=void 0,this._abandonRulesCheck=()=>{const{fragCurrent:t,partCurrent:e,hls:s}=this,{autoLevelEnabled:i,media:r}=s;if(!t||!r)return;const n=performance.now(),a=e?e.stats:t.stats,o=e?e.duration:t.duration,l=n-a.loading.start,h=s.minAutoLevel;if(a.aborted||a.loaded&&a.loaded===a.total||t.level<=h)return this.clearTimer(),void(this._nextAutoLevel=-1);if(!i||r.paused||!r.playbackRate||!r.readyState)return;const d=s.mainForwardBufferInfo;if(null===d)return;const c=this.bwEstimator.getEstimateTTFB(),u=Math.abs(r.playbackRate);if(l<=Math.max(c,o/(2*u)*1e3))return;const g=d.len/u,m=a.loading.first?a.loading.first-a.loading.start:-1,v=a.loaded&&m>-1,y=this.getBwEstimate(),E=s.levels,T=E[t.level],S=a.total||Math.max(a.loaded,Math.round(o*T.averageBitrate/8));let L=v?l-m:l;L<1&&v&&(L=Math.min(l,8*a.loaded/y));const R=v?1e3*a.loaded/L:0,b=R?(S-a.loaded)/R:8*S/y+c/1e3;if(b<=g)return;const k=R?8*R:y;let w,D=Number.POSITIVE_INFINITY;for(w=t.level-1;w>h;w--){const t=E[w].maxBitrate;if(D=this.getTimeToLoadFrag(c/1e3,k,o*t,!E[w].details),D=b)return;if(D>10*o)return;s.nextLoadLevel=s.nextAutoLevel=w,v?this.bwEstimator.sample(l-Math.min(c,m),a.loaded):this.bwEstimator.sampleTTFB(l);const I=E[w].maxBitrate;this.getBwEstimate()*this.hls.config.abrBandWidthUpFactor>I&&this.resetEstimator(I),this.clearTimer(),A.warn(`[abr] Fragment ${t.sn}${e?" part "+e.index:""} of level ${t.level} is loading too slowly;\n Time to underbuffer: ${g.toFixed(3)} s\n Estimated load time for current fragment: ${b.toFixed(3)} s\n Estimated load time for down switch fragment: ${D.toFixed(3)} s\n TTFB estimate: ${0|m} ms\n Current BW estimate: ${f(y)?0|y:"Unknown"} bps\n New BW estimate: ${0|this.getBwEstimate()} bps\n Switching to level ${w} @ ${0|I} bps`),s.trigger(p.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:e,stats:a})},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}resetEstimator(t){t&&(A.log(`setting initial bwe to ${t}`),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()}initEstimator(){const t=this.hls.config;return new Is(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.FRAG_LOADING,this.onFragLoading,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t&&(t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.FRAG_LOADING,this.onFragLoading,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(p.ERROR,this.onError,this))}destroy(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null}onManifestLoading(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()}onLevelsUpdated(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null}onMaxAutoLevelUpdated(){this.firstSelection=-1,this.nextAutoLevelKey=""}onFragLoading(t,e){const s=e.frag;if(!this.ignoreFragment(s)){var i;if(!s.bitrateTest)this.fragCurrent=s,this.partCurrent=null!=(i=e.part)?i:null;this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100)}}onLevelSwitching(t,e){this.clearTimer()}onError(t,e){if(!e.fatal)switch(e.details){case y.BUFFER_ADD_CODEC_ERROR:case y.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case y.FRAG_LOAD_TIMEOUT:{const t=e.frag,{fragCurrent:s,partCurrent:i}=this;if(t&&s&&t.sn===s.sn&&t.level===s.level){const e=performance.now(),s=i?i.stats:t.stats,r=e-s.loading.start,n=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&n>-1){const t=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(r-Math.min(t,n),s.loaded)}else this.bwEstimator.sampleTTFB(r)}break}}}getTimeToLoadFrag(t,e,s,i){return t+s/e+(i?this.lastLevelLoadSec:0)}onLevelLoaded(t,e){const s=this.hls.config,{loading:i}=e.stats,r=i.end-i.start;f(r)&&(this.lastLevelLoadSec=r/1e3),e.details.live?this.bwEstimator.update(s.abrEwmaSlowLive,s.abrEwmaFastLive):this.bwEstimator.update(s.abrEwmaSlowVoD,s.abrEwmaFastVoD)}onFragLoaded(t,{frag:e,part:s}){const i=s?s.stats:e.stats;if(e.type===De&&this.bwEstimator.sampleTTFB(i.loading.first-i.loading.start),!this.ignoreFragment(e)){if(this.clearTimer(),e.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){const t=s?s.duration:e.duration,r=this.hls.levels[e.level],n=(r.loaded?r.loaded.bytes:0)+i.loaded,a=(r.loaded?r.loaded.duration:0)+t;r.loaded={bytes:n,duration:a},r.realBitrate=Math.round(8*n/a)}if(e.bitrateTest){const t={stats:i,frag:e,part:s,id:e.type};this.onFragBuffered(p.FRAG_BUFFERED,t),e.bitrateTest=!1}else this.lastLoadedFragLevel=e.level}}onFragBuffered(t,e){const{frag:s,part:i}=e,r=null!=i&&i.stats.loaded?i.stats:s.stats;if(r.aborted)return;if(this.ignoreFragment(s))return;const n=r.parsing.end-r.loading.start-Math.min(r.loading.first-r.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(n,r.loaded),r.bwEstimate=this.getBwEstimate(),s.bitrateTest?this.bitrateTestDelay=n/1e3:this.bitrateTestDelay=0}ignoreFragment(t){return t.type!==De||"initSegment"===t.sn}clearTimer(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)}get firstAutoLevel(){const{maxAutoLevel:t,minAutoLevel:e}=this.hls,s=this.getBwEstimate(),i=this.hls.config.maxStarvationDelay,r=this.findBestLevel(s,e,t,0,i,1,1);if(r>-1)return r;const n=this.hls.firstLevel,a=Math.min(Math.max(n,e),t);return A.warn(`[abr] Could not find best starting auto level. Defaulting to first in playlist ${n} clamped to ${a}`),a}get forcedAutoLevel(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}get nextAutoLevel(){const t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),s=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&s&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;const i=e&&s?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){const e=this.hls.levels;if(e.length>Math.max(t,i)&&e[t].loadError<=e[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i}getAutoLevelKey(){return`${this.getBwEstimate()}_${this.getStarvationDelay().toFixed(2)}`}getNextABRAutoLevel(){const{fragCurrent:t,partCurrent:e,hls:s}=this,{maxAutoLevel:i,config:r,minAutoLevel:n}=s,a=e?e.duration:t?t.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay();let h=r.abrBandWidthFactor,d=r.abrBandWidthUpFactor;if(l){const t=this.findBestLevel(o,n,i,l,0,h,d);if(t>=0)return t}let c=a?Math.min(a,r.maxStarvationDelay):r.maxStarvationDelay;if(!l){const t=this.bitrateTestDelay;if(t){c=(a?Math.min(a,r.maxLoadingDelay):r.maxLoadingDelay)-t,A.info(`[abr] bitrate test took ${Math.round(1e3*t)}ms, set first fragment max fetchDuration to ${Math.round(1e3*c)} ms`),h=d=1}}const u=this.findBestLevel(o,n,i,l,c,h,d);if(A.info(`[abr] ${l?"rebuffering expected":"buffer is empty"}, optimal quality level ${u}`),u>-1)return u;const f=s.levels[n],g=s.levels[s.loadLevel];return(null==f?void 0:f.bitrate)<(null==g?void 0:g.bitrate)?n:s.loadLevel}getStarvationDelay(){const t=this.hls,e=t.media;if(!e)return 1/0;const s=e&&0!==e.playbackRate?Math.abs(e.playbackRate):1,i=t.mainForwardBufferInfo;return(i?i.len:0)/s}getBwEstimate(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate}findBestLevel(t,e,s,i,r,n,a){var o;const l=i+r,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,{fragCurrent:c,partCurrent:u}=this,{levels:g,allAudioTracks:m,loadLevel:p,config:v}=this.hls;if(1===g.length)return 0;const y=g[d],E=!(null==y||null==(o=y.details)||!o.live),T=-1===p||-1===h;let S,L="SDR",R=(null==y?void 0:y.frameRate)||0;const{audioPreference:b,videoPreference:k}=v,w=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce(((t,e)=>{let s=t.groups[e.groupId];s||(s=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),s.tracks.push(e);const i=e.channels||"2";return s.channels[i]=(s.channels[i]||0)+1,s.hasDefault=s.hasDefault||e.default,s.hasAutoSelect=s.hasAutoSelect||e.autoselect,s.hasDefault&&(t.hasDefaultAudio=!0),s.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(T){if(-1!==this.firstSelection)return this.firstSelection;const i=this.codecTiers||(this.codecTiers=function(t,e,s,i){return t.slice(s,i+1).reduce(((t,s)=>{if(!s.codecSet)return t;const i=s.audioGroups;let r=t[s.codecSet];r||(t[s.codecSet]=r={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),r.minBitrate=Math.min(r.minBitrate,s.bitrate);const n=Math.min(s.height,s.width);return r.minHeight=Math.min(r.minHeight,n),r.minFramerate=Math.min(r.minFramerate,s.frameRate),r.maxScore=Math.max(r.maxScore,s.score),r.fragmentError+=s.fragmentError,r.videoRanges[s.videoRange]=(r.videoRanges[s.videoRange]||0)+1,i&&i.forEach((t=>{if(!t)return;const s=e.groups[t];s&&(r.hasDefaultAudio=r.hasDefaultAudio||e.hasDefaultAudio?s.hasDefault:s.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(s.channels).forEach((t=>{r.channels[t]=(r.channels[t]||0)+s.channels[t]})))})),t}),{})}(g,w,e,s)),r=function(t,e,s,i,r){const n=Object.keys(t),a=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=a&&2===parseInt(a);let h=!0,d=!1,c=1/0,u=1/0,g=1/0,m=0,p=[];const{preferHDR:v,allowedVideoRanges:y}=Ms(e,r);for(let e=n.length;e--;){const s=t[n[e]];h=s.channels[2]>0,c=Math.min(c,s.minHeight),u=Math.min(u,s.minFramerate),g=Math.min(g,s.minBitrate);const i=y.filter((t=>s.videoRanges[t]>0));i.length>0&&(d=!0,p=i)}c=f(c)?c:0,u=f(u)?u:0;const E=Math.max(1080,c),T=Math.max(30,u);return g=f(g)?g:s,s=Math.max(g,s),d||(e=void 0,p=[]),{codecSet:n.reduce(((e,i)=>{const r=t[i];if(i===e)return e;if(r.minBitrate>s)return Fs(i,`min bitrate of ${r.minBitrate} > current estimate of ${s}`),e;if(!r.hasDefaultAudio)return Fs(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Fs(i,`audio codec preference "${o}" not found`),e;if(a&&!l){if(!r.channels[a])return Fs(i,`no renditions with ${a} channel sound found (channels options: ${Object.keys(r.channels)})`),e}else if((!o||l)&&h&&0===r.channels[2])return Fs(i,"no renditions with stereo sound found"),e;return r.minHeight>E?(Fs(i,`min resolution of ${r.minHeight} > maximum of ${E}`),e):r.minFramerate>T?(Fs(i,`min framerate of ${r.minFramerate} > maximum of ${T}`),e):p.some((t=>r.videoRanges[t]>0))?r.maxScore=ae(e)||r.fragmentError>t[e].fragmentError)?e:(m=r.maxScore,i):(Fs(i,`no variants with VIDEO-RANGE of ${JSON.stringify(p)} found`),e)}),void 0),videoRanges:p,preferHDR:v,minFramerate:u,minBitrate:g}}(i,L,t,b,k),{codecSet:n,videoRanges:a,minFramerate:o,minBitrate:l,preferHDR:h}=r;S=n,L=h?a[a.length-1]:a[0],R=o,t=Math.max(t,l),A.log(`[abr] picked start tier ${JSON.stringify(r)}`)}else S=null==y?void 0:y.codecSet,L=null==y?void 0:y.videoRange;const D=u?u.duration:c?c.duration:0,I=this.bwEstimator.getEstimateTTFB()/1e3,_=[];for(let o=s;o>=e;o--){var C;const e=g[o],c=o>d;if(!e)continue;if(v.useMediaCapabilities&&!e.supportedResult&&!e.supportedPromise){const s=navigator.mediaCapabilities;"function"==typeof(null==s?void 0:s.decodingInfo)&&xs(e,w,L,R,t,b)?(e.supportedPromise=Ps(e,w,s),e.supportedPromise.then((t=>{if(!this.hls)return;e.supportedResult=t;const s=this.hls.levels,i=s.indexOf(e);t.error?A.warn(`[abr] MediaCapabilities decodingInfo error: "${t.error}" for level ${i} ${JSON.stringify(t)}`):t.supported||(A.warn(`[abr] Unsupported MediaCapabilities decodingInfo result for level ${i} ${JSON.stringify(t)}`),i>-1&&s.length>1&&(A.log(`[abr] Removing unsupported level ${i}`),this.hls.removeLevel(i)))}))):e.supportedResult=_s}if(S&&e.codecSet!==S||L&&e.videoRange!==L||c&&R>e.frameRate||!c&&R>0&&R=2*D&&0===r?g[o].averageBitrate:g[o].maxBitrate,M=this.getTimeToLoadFrag(I,x,P*k,void 0===m);if(x>=P&&(o===h||0===e.loadError&&0===e.fragmentError)&&(M<=I||!f(M)||E&&!this.bitrateTestDelay||M${o} adjustedbw(${Math.round(x)})-bitrate=${Math.round(x-P)} ttfb:${I.toFixed(1)} avgDuration:${k.toFixed(1)} maxFetchDuration:${l.toFixed(1)} fetchDuration:${M.toFixed(1)} firstSelection:${T} codecSet:${S} videoRange:${L} hls.loadLevel:${p}`)),T&&(this.firstSelection=o),o}}return-1}set nextAutoLevel(t){const{maxAutoLevel:e,minAutoLevel:s}=this.hls,i=Math.min(Math.max(t,s),e);this._nextAutoLevel!==i&&(this.nextAutoLevelKey="",this._nextAutoLevel=i)}},bufferController:class{constructor(t){this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=t=>{this.hls&&this.hls.pauseBuffering()},this._onStartStreaming=t=>{this.hls&&this.hls.resumeBuffering()},this._onMediaSourceOpen=()=>{const{media:t,mediaSource:e}=this;this.log("Media source opened"),t&&(t.removeEventListener("emptied",this._onMediaEmptied),this.updateMediaElementDuration(),this.hls.trigger(p.MEDIA_ATTACHED,{media:t,mediaSource:e})),e&&e.removeEventListener("sourceopen",this._onMediaSourceOpen),this.checkPendingTracks()},this._onMediaSourceClose=()=>{this.log("Media source closed")},this._onMediaSourceEnded=()=>{this.log("Media source ended")},this._onMediaEmptied=()=>{const{mediaSrc:t,_objectUrl:e}=this;t!==e&&A.error(`Media element src was set while attaching MediaSource (${e} > ${t})`)},this.hls=t;const e="[buffer-controller]";var s;this.appendSource=(s=te(t.config.preferManagedMediaSource),"undefined"!=typeof self&&s===self.ManagedMediaSource),this.log=A.log.bind(A,e),this.warn=A.warn.bind(A,e),this.error=A.error.bind(A,e),this._initSourceBuffer(),this.registerListeners()}hasSourceTypes(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0}destroy(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null}registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.BUFFER_RESET,this.onBufferReset,this),t.on(p.BUFFER_APPENDING,this.onBufferAppending,this),t.on(p.BUFFER_CODECS,this.onBufferCodecs,this),t.on(p.BUFFER_EOS,this.onBufferEos,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(p.FRAG_PARSED,this.onFragParsed,this),t.on(p.FRAG_CHANGED,this.onFragChanged,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.BUFFER_RESET,this.onBufferReset,this),t.off(p.BUFFER_APPENDING,this.onBufferAppending,this),t.off(p.BUFFER_CODECS,this.onBufferCodecs,this),t.off(p.BUFFER_EOS,this.onBufferEos,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(p.FRAG_PARSED,this.onFragParsed,this),t.off(p.FRAG_CHANGED,this.onFragChanged,this)}_initSourceBuffer(){this.sourceBuffer={},this.operationQueue=new Or(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null}onManifestLoading(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null}onManifestParsed(t,e){let s=2;(e.audio&&!e.video||!e.altAudio)&&(s=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=s,this.log(`${this.bufferCodecEventsExpected} bufferCodec event(s) expected`)}onMediaAttaching(t,e){const s=this.media=e.media,i=te(this.appendSource);if(s&&i){var r;const t=this.mediaSource=new i;this.log(`created media source: ${null==(r=t.constructor)?void 0:r.name}`),t.addEventListener("sourceopen",this._onMediaSourceOpen),t.addEventListener("sourceended",this._onMediaSourceEnded),t.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(t.addEventListener("startstreaming",this._onStartStreaming),t.addEventListener("endstreaming",this._onEndStreaming));const e=this._objectUrl=self.URL.createObjectURL(t);if(this.appendSource)try{s.removeAttribute("src");const i=self.ManagedMediaSource;s.disableRemotePlayback=s.disableRemotePlayback||i&&t instanceof i,Ur(s),function(t,e){const s=self.document.createElement("source");s.type="video/mp4",s.src=e,t.appendChild(s)}(s,e),s.load()}catch(t){s.src=e}else s.src=e;s.addEventListener("emptied",this._onMediaEmptied)}}onMediaDetaching(){const{media:t,mediaSource:e,_objectUrl:s}=this;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn(`onMediaDetaching: ${t.message} while calling endOfStream`)}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming)),t&&(t.removeEventListener("emptied",this._onMediaEmptied),s&&self.URL.revokeObjectURL(s),this.mediaSrc===s?(t.removeAttribute("src"),this.appendSource&&Ur(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(p.MEDIA_DETACHED,void 0)}onBufferReset(){this.getSourceBufferTypes().forEach((t=>{this.resetBuffer(t)})),this._initSourceBuffer()}resetBuffer(t){const e=this.sourceBuffer[t];try{var s;if(e)this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(s=this.mediaSource)&&s.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e)}catch(e){this.warn(`onBufferReset ${t}`,e)}}onBufferCodecs(t,e){const s=this.getSourceBufferTypes().length,i=Object.keys(e);if(i.forEach((t=>{if(s){const s=this.tracks[t];if(s&&"function"==typeof s.buffer.changeType){var i;const{id:r,codec:n,levelCodec:a,container:o,metadata:l}=e[t],h=de(s.codec,s.levelCodec),d=null==h?void 0:h.replace(Nr,"$1");let c=de(n,a);const u=null==(i=c)?void 0:i.replace(Nr,"$1");if(c&&d!==u){"audio"===t.slice(0,5)&&(c=he(c,this.appendSource));const e=`${o};codecs=${c}`;this.appendChangeType(t,e),this.log(`switching codec ${h} to ${c}`),this.tracks[t]={buffer:s.buffer,codec:n,container:o,levelCodec:a,metadata:l,id:r}}}}else this.pendingTracks[t]=e[t]})),s)return;const r=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==r&&(this.log(`${r} bufferCodec event(s) expected ${i.join(",")}`),this.bufferCodecEventsExpected=r),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}appendChangeType(t,e){const{operationQueue:s}=this,i={execute:()=>{const i=this.sourceBuffer[t];i&&(this.log(`changing ${t} sourceBuffer type to ${e}`),i.changeType(e)),s.shiftAndExecuteNext(t)},onStart:()=>{},onComplete:()=>{},onError:e=>{this.warn(`Failed to change ${t} SourceBuffer type`,e)}};s.append(i,t,!!this.pendingTracks[t])}onBufferAppending(t,e){const{hls:s,operationQueue:i,tracks:r}=this,{data:n,type:a,frag:o,part:l,chunkMeta:h}=e,d=h.buffering[a],c=self.performance.now();d.start=c;const u=o.stats.buffering,f=l?l.stats.buffering:null;0===u.start&&(u.start=c),f&&0===f.start&&(f.start=c);const g=r.audio;let m=!1;"audio"===a&&"audio/mpeg"===(null==g?void 0:g.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);const E=o.start,T={execute:()=>{if(d.executeStart=self.performance.now(),m){const t=this.sourceBuffer[a];if(t){const e=E-t.timestampOffset;Math.abs(e)>=.1&&(this.log(`Updating audio SourceBuffer timestampOffset to ${E} (delta: ${e}) sn: ${o.sn})`),t.timestampOffset=E)}}this.appendExecutor(n,a)},onStart:()=>{},onComplete:()=>{const t=self.performance.now();d.executeEnd=d.end=t,0===u.first&&(u.first=t),f&&0===f.first&&(f.first=t);const{sourceBuffer:e}=this,s={};for(const t in e)s[t]=Xs.getBuffered(e[t]);this.appendErrors[a]=0,"audio"===a||"video"===a?this.appendErrors.audiovideo=0:(this.appendErrors.audio=0,this.appendErrors.video=0),this.hls.trigger(p.BUFFER_APPENDED,{type:a,frag:o,part:l,chunkMeta:h,parent:o.type,timeRanges:s})},onError:t=>{const e={type:v.MEDIA_ERROR,parent:o.type,details:y.BUFFER_APPEND_ERROR,sourceBufferName:a,frag:o,part:l,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=y.BUFFER_FULL_ERROR;else{const t=++this.appendErrors[a];e.details=y.BUFFER_APPEND_ERROR,this.warn(`Failed ${t}/${s.config.appendErrorMaxRetry} times to append segment in "${a}" sourceBuffer`),t>=s.config.appendErrorMaxRetry&&(e.fatal=!0)}s.trigger(p.ERROR,e)}};i.append(T,a,!!this.pendingTracks[a])}onBufferFlushing(t,e){const{operationQueue:s}=this,i=t=>({execute:this.removeExecutor.bind(this,t,e.startOffset,e.endOffset),onStart:()=>{},onComplete:()=>{this.hls.trigger(p.BUFFER_FLUSHED,{type:t})},onError:e=>{this.warn(`Failed to remove from ${t} SourceBuffer`,e)}});e.type?s.append(i(e.type),e.type):this.getSourceBufferTypes().forEach((t=>{s.append(i(t),t)}))}onFragParsed(t,e){const{frag:s,part:i}=e,r=[],n=i?i.elementaryStreams:s.elementaryStreams;n[x]?r.push("audiovideo"):(n[_]&&r.push("audio"),n[C]&&r.push("video"));0===r.length&&this.warn(`Fragments must have at least one ElementaryStreamType set. type: ${s.type} level: ${s.level} sn: ${s.sn}`),this.blockBuffers((()=>{const t=self.performance.now();s.stats.buffering.end=t,i&&(i.stats.buffering.end=t);const e=i?i.stats:s.stats;this.hls.trigger(p.FRAG_BUFFERED,{frag:s,part:i,stats:e,id:s.type})}),r)}onFragChanged(t,e){this.trimBuffers()}onBufferEos(t,e){this.getSourceBufferTypes().reduce(((t,s)=>{const i=this.sourceBuffer[s];return!i||e.type&&e.type!==s||(i.ending=!0,i.ended||(i.ended=!0,this.log(`${s} sourceBuffer now EOS`))),t&&!(i&&!i.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((()=>{this.getSourceBufferTypes().forEach((t=>{const e=this.sourceBuffer[t];e&&(e.ending=!1)}));const{mediaSource:t}=this;t&&"open"===t.readyState?(this.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&this.log(`Could not call mediaSource.endOfStream(). mediaSource.readyState: ${t.readyState}`)})))}onLevelUpdated(t,{details:e}){e.fragments.length&&(this.details=e,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())}trimBuffers(){const{hls:t,details:e,media:s}=this;if(!s||null===e)return;if(!this.getSourceBufferTypes().length)return;const i=t.config,r=s.currentTime,n=e.levelTargetDuration,a=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(f(a)&&a>0){const t=Math.max(a,n),e=Math.floor(r/n)*n-t;this.flushBackBuffer(r,n,e)}if(f(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){const t=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),e=Math.max(t,n),s=Math.floor(r/n)*n+e;this.flushFrontBuffer(r,n,s)}}flushBackBuffer(t,e,s){const{details:i,sourceBuffer:r}=this;this.getSourceBufferTypes().forEach((n=>{const a=r[n];if(a){const r=Xs.getBuffered(a);if(r.length>0&&s>r.start(0)){if(this.hls.trigger(p.BACK_BUFFER_REACHED,{bufferEnd:s}),null!=i&&i.live)this.hls.trigger(p.LIVE_BACK_BUFFER_REACHED,{bufferEnd:s});else if(a.ended&&r.end(r.length-1)-t<2*e)return void this.log(`Cannot flush ${n} back buffer while SourceBuffer is in ended state`);this.hls.trigger(p.BUFFER_FLUSHING,{startOffset:0,endOffset:s,type:n})}}}))}flushFrontBuffer(t,e,s){const{sourceBuffer:i}=this;this.getSourceBufferTypes().forEach((r=>{const n=i[r];if(n){const i=Xs.getBuffered(n),a=i.length;if(a<2)return;const o=i.start(a-1),l=i.end(a-1);if(s>o||t>=o&&t<=l)return;if(n.ended&&t-l<2*e)return void this.log(`Cannot flush ${r} front buffer while SourceBuffer is in ended state`);this.hls.trigger(p.BUFFER_FLUSHING,{startOffset:o,endOffset:1/0,type:r})}}))}updateMediaElementDuration(){if(!this.details||!this.media||!this.mediaSource||"open"!==this.mediaSource.readyState)return;const{details:t,hls:e,media:s,mediaSource:i}=this,r=t.fragments[0].start+t.totalduration,n=s.duration,a=f(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(r>a&&r>n||!f(n))&&(this.log(`Updating Media Source duration to ${r.toFixed(3)}`),i.duration=r)}updateSeekableRange(t){const e=this.mediaSource,s=t.fragments;if(s.length&&t.live&&null!=e&&e.setLiveSeekableRange){const i=Math.max(0,s[0].start),r=Math.max(i,i+t.totalduration);this.log(`Media Source duration is set to ${e.duration}. Setting seekable range to ${i}-${r}.`),e.setLiveSeekableRange(i,r)}}checkPendingTracks(){const{bufferCodecEventsExpected:t,operationQueue:e,pendingTracks:s}=this,i=Object.keys(s).length;if(i&&(!t||2===i||"audiovideo"in s)){this.createSourceBuffers(s),this.pendingTracks={};const t=this.getSourceBufferTypes();if(t.length)this.hls.trigger(p.BUFFER_CREATED,{tracks:this.tracks}),t.forEach((t=>{e.executeNext(t)}));else{const t=new Error("could not create source buffer for media codec(s)");this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:t,reason:t.message})}}}createSourceBuffers(t){const{sourceBuffer:e,mediaSource:s}=this;if(!s)throw Error("createSourceBuffers called when mediaSource was null");for(const r in t)if(!e[r]){var i;const n=t[r];if(!n)throw Error(`source buffer exists for track ${r}, however track does not`);let a=-1===(null==(i=n.levelCodec)?void 0:i.indexOf(","))?n.levelCodec:n.codec;a&&"audio"===r.slice(0,5)&&(a=he(a,this.appendSource));const o=`${n.container};codecs=${a}`;this.log(`creating sourceBuffer(${o})`);try{const t=e[r]=s.addSourceBuffer(o),i=r;this.addBufferListener(i,"updatestart",this._onSBUpdateStart),this.addBufferListener(i,"updateend",this._onSBUpdateEnd),this.addBufferListener(i,"error",this._onSBUpdateError),this.appendSource&&this.addBufferListener(i,"bufferedchange",((t,e)=>{const s=e.removedRanges;null!=s&&s.length&&this.hls.trigger(p.BUFFER_FLUSHED,{type:r})})),this.tracks[r]={buffer:t,codec:a,container:n.container,levelCodec:n.levelCodec,metadata:n.metadata,id:n.id}}catch(t){this.error(`error while trying to add sourceBuffer: ${t.message}`),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:r,mimeType:o})}}}get mediaSrc(){var t,e;const s=(null==(t=this.media)||null==(e=t.querySelector)?void 0:e.call(t,"source"))||this.media;return null==s?void 0:s.src}_onSBUpdateStart(t){const{operationQueue:e}=this;e.current(t).onStart()}_onSBUpdateEnd(t){var e;if("closed"===(null==(e=this.mediaSource)?void 0:e.readyState))return void this.resetBuffer(t);const{operationQueue:s}=this;s.current(t).onComplete(),s.shiftAndExecuteNext(t)}_onSBUpdateError(t,e){var s;const i=new Error(`${t} SourceBuffer error. MediaSource readyState: ${null==(s=this.mediaSource)?void 0:s.readyState}`);this.error(`${i}`,e),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});const r=this.operationQueue.current(t);r&&r.onError(i)}removeExecutor(t,e,s){const{media:i,mediaSource:r,operationQueue:n,sourceBuffer:a}=this,o=a[t];if(!i||!r||!o)return this.warn(`Attempting to remove from the ${t} SourceBuffer, but it does not exist`),void n.shiftAndExecuteNext(t);const l=f(i.duration)?i.duration:1/0,h=f(r.duration)?r.duration:1/0,d=Math.max(0,e),c=Math.min(s,l,h);c>d&&(!o.ending||o.ended)?(o.ended=!1,this.log(`Removing [${d},${c}] from the ${t} SourceBuffer`),o.remove(d,c)):n.shiftAndExecuteNext(t)}appendExecutor(t,e){const s=this.sourceBuffer[e];if(s)s.ended=!1,s.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error(`Attempting to append to the ${e} SourceBuffer, but it does not exist`)}blockBuffers(t,e=this.getSourceBufferTypes()){if(!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);const{operationQueue:s}=this,i=e.map((t=>s.appendBlocker(t)));Promise.all(i).then((()=>{t(),e.forEach((t=>{const e=this.sourceBuffer[t];null!=e&&e.updating||s.shiftAndExecuteNext(t)}))}))}getSourceBufferTypes(){return Object.keys(this.sourceBuffer)}addBufferListener(t,e,s){const i=this.sourceBuffer[t];if(!i)return;const r=s.bind(this,t);this.listeners[t].push({event:e,listener:r}),i.addEventListener(e,r)}removeBufferListeners(t){const e=this.sourceBuffer[t];e&&this.listeners[t].forEach((t=>{e.removeEventListener(t.event,t.listener)}))}},capLevelController:Mn,errorController:class{constructor(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=A.log.bind(A,"[info]:"),this.warn=A.warn.bind(A,"[warning]:"),this.error=A.error.bind(A,"[error]:"),this.registerListeners()}registerListeners(){const t=this.hls;t.on(p.ERROR,this.onError,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this)}unregisterListeners(){const t=this.hls;t&&(t.off(p.ERROR,this.onError,this),t.off(p.ERROR,this.onErrorOut,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this))}destroy(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}}startLoad(t){}stopLoad(){this.playlistError=0}getVariantLevelIndex(t){return(null==t?void 0:t.type)===De?t.level:this.hls.loadLevel}onManifestLoading(){this.playlistError=0,this.penalizedRenditions={}}onLevelUpdated(){this.playlistError=0}onError(t,e){var s,i;if(e.fatal)return;const r=this.hls,n=e.context;switch(e.details){case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case y.FRAG_PARSING_ERROR:if(null!=(s=e.frag)&&s.gap)return void(e.errorAction={action:Ts,flags:Rs});case y.FRAG_GAP:case y.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Ss);case y.LEVEL_EMPTY_ERROR:case y.LEVEL_PARSING_ERROR:{var a,o;const t=e.parent===De?e.level:r.loadLevel;e.details===y.LEVEL_EMPTY_ERROR&&null!=(a=e.context)&&null!=(o=a.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,t):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,t))}return;case y.LEVEL_LOAD_ERROR:case y.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==n?void 0:n.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.level)));case y.AUDIO_TRACK_LOAD_ERROR:case y.AUDIO_TRACK_LOAD_TIMEOUT:case y.SUBTITLE_LOAD_ERROR:case y.SUBTITLE_TRACK_LOAD_TIMEOUT:if(n){const t=r.levels[r.loadLevel];if(t&&(n.type===ke&&t.hasAudioGroup(n.groupId)||n.type===we&&t.hasSubtitleGroup(n.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,r.loadLevel),e.errorAction.action=Ss,void(e.errorAction.flags=bs)}return;case y.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:{const t=r.levels[r.loadLevel],s=null==t?void 0:t.attrs["HDCP-LEVEL"];s?e.errorAction={action:Ss,flags:ks,hdcpLevel:s}:this.keySystemError(e)}return;case y.BUFFER_ADD_CODEC_ERROR:case y.REMUX_ALLOC_ERROR:case y.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:r.loadLevel));case y.INTERNAL_EXCEPTION:case y.BUFFER_APPENDING_ERROR:case y.BUFFER_FULL_ERROR:case y.LEVEL_SWITCH_ERROR:case y.BUFFER_STALLED_ERROR:case y.BUFFER_SEEK_OVER_HOLE:case y.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:Ts,flags:Rs})}e.type===v.KEY_SYSTEM_ERROR&&this.keySystemError(e)}keySystemError(t){const e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)}getPlaylistRetryOrSwitchAction(t,e){const s=us(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(ms(s,i,cs(t),t.response))return{action:As,flags:Rs,retryConfig:s,retryCount:i};const r=this.getLevelSwitchAction(t,e);return s&&(r.retryConfig=s,r.retryCount=i),r}getFragRetryOrSwitchAction(t){const e=this.hls,s=this.getVariantLevelIndex(t.frag),i=e.levels[s],{fragLoadPolicy:r,keyLoadPolicy:n}=e.config,a=us(t.details.startsWith("key")?n:r,t),o=e.levels.reduce(((t,e)=>t+e.fragmentError),0);if(i){t.details!==y.FRAG_GAP&&i.fragmentError++;if(ms(a,o,cs(t),t.response))return{action:As,flags:Rs,retryConfig:a,retryCount:o}}const l=this.getLevelSwitchAction(t,s);return a&&(l.retryConfig=a,l.retryCount=o),l}getLevelSwitchAction(t,e){const s=this.hls;null==e&&(e=s.loadLevel);const i=this.hls.levels[e];if(i){var r,n;const e=t.details;i.loadError++,e===y.BUFFER_APPEND_ERROR&&i.fragmentError++;let l=-1;const{levels:h,loadLevel:d,minAutoLevel:c,maxAutoLevel:u}=s;s.autoLevelEnabled||(s.loadLevel=-1);const f=null==(r=t.frag)?void 0:r.type,g=(f===Ie&&e===y.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(e===y.BUFFER_ADD_CODEC_ERROR||e===y.BUFFER_APPEND_ERROR))&&h.some((({audioCodec:t})=>i.audioCodec!==t)),m="video"===t.sourceBufferName&&(e===y.BUFFER_ADD_CODEC_ERROR||e===y.BUFFER_APPEND_ERROR)&&h.some((({codecSet:t,audioCodec:e})=>i.codecSet!==t&&i.audioCodec===e)),{type:p,groupId:v}=null!=(n=t.context)?n:{};for(let s=h.length;s--;){const r=(s+d)%h.length;if(r!==d&&r>=c&&r<=u&&0===h[r].loadError){var a,o;const s=h[r];if(e===y.FRAG_GAP&&f===De&&t.frag){const e=h[r].details;if(e){const s=vs(t.frag,e.fragments,t.frag.start);if(null!=s&&s.gap)continue}}else{if(p===ke&&s.hasAudioGroup(v)||p===we&&s.hasSubtitleGroup(v))continue;if(f===Ie&&null!=(a=i.audioGroups)&&a.some((t=>s.hasAudioGroup(t)))||f===_e&&null!=(o=i.subtitleGroups)&&o.some((t=>s.hasSubtitleGroup(t)))||g&&i.audioCodec===s.audioCodec||!g&&i.audioCodec!==s.audioCodec||m&&i.codecSet===s.codecSet)continue}l=r;break}}if(l>-1&&s.loadLevel!==l)return t.levelRetry=!0,this.playlistError=0,{action:Ss,flags:Rs,nextAutoLevel:l}}return{action:Ss,flags:bs}}onErrorOut(t,e){var s;switch(null==(s=e.errorAction)?void 0:s.action){case Ts:break;case Ss:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===y.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn(`MediaSource ended after "${e.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()}sendAlternateToPenaltyBox(t){const e=this.hls,s=t.errorAction;if(!s)return;const{flags:i,hdcpLevel:r,nextAutoLevel:n}=s;switch(i){case Rs:this.switchLevel(t,n);break;case ks:r&&(e.maxHdcpLevel=qe[qe.indexOf(r)-1],s.resolved=!0),this.warn(`Restricting playback to HDCP-LEVEL of "${e.maxHdcpLevel}" or lower`)}s.resolved||this.switchLevel(t,n)}switchLevel(t,e){void 0!==e&&t.errorAction&&(this.warn(`switching to level ${e} after ${t.details}`),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)}},fpsController:class{constructor(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}setStreamController(t){this.streamController=t}registerListeners(){this.hls.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this)}destroy(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null}onMediaAttaching(t,e){const s=this.hls.config;if(s.capLevelOnFPSDrop){const t=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=t,t&&"function"==typeof t.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),s.fpsDroppedMonitoringPeriod)}}checkFPS(t,e,s){const i=performance.now();if(e){if(this.lastTime){const t=i-this.lastTime,r=s-this.lastDroppedFrames,n=e-this.lastDecodedFrames,a=1e3*r/t,o=this.hls;if(o.trigger(p.FPS_DROP,{currentDropped:r,currentDecoded:n,totalDroppedFrames:s}),a>0&&r>o.config.fpsDroppedMonitoringThreshold*n){let t=o.currentLevel;A.warn("drop FPS ratio greater than max allowed value for currentLevel: "+t),t>0&&(-1===o.autoLevelCapping||o.autoLevelCapping>=t)&&(t-=1,o.trigger(p.FPS_DROP_LEVEL_CAPPING,{level:t,droppedLevel:o.currentLevel}),o.autoLevelCapping=t,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=s,this.lastDecodedFrames=e}}checkFPSInterval(){const t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){const e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)}},stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:tt,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:Sa,enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:class extends Ri{constructor(t,e,s){super(t,e,s,"[subtitle-stream-controller]",_e),this.currentTrackId=-1,this.tracksBuffered=[],this.mainDetails=null,this._registerListeners()}onHandlerDestroying(){this._unregisterListeners(),super.onHandlerDestroying(),this.mainDetails=null}_registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.ERROR,this.onError,this),t.on(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(p.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.on(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(p.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.ERROR,this.onError,this),t.off(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(p.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.off(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(p.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this)}startLoad(t){this.stopLoad(),this.state=fi,this.setInterval(500),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}onManifestLoading(){this.mainDetails=null,this.fragmentTracker.removeAllFragments()}onMediaDetaching(){this.tracksBuffered=[],super.onMediaDetaching()}onLevelLoaded(t,e){this.mainDetails=e.details}onSubtitleFragProcessed(t,e){const{frag:s,success:i}=e;if(this.fragPrevious=s,this.state=fi,!i)return;const r=this.tracksBuffered[this.currentTrackId];if(!r)return;let n;const a=s.start;for(let t=0;t=r[t].start&&a<=r[t].end){n=r[t];break}const o=s.start+s.duration;n?n.end=o:(n={start:a,end:o},r.push(n)),this.fragmentTracker.fragBuffered(s),this.fragBufferedComplete(s,null)}onBufferFlushing(t,e){const{startOffset:s,endOffset:i}=e;if(0===s&&i!==Number.POSITIVE_INFINITY){const t=i-1;if(t<=0)return;e.endOffsetSubtitles=Math.max(0,t),this.tracksBuffered.forEach((e=>{for(let s=0;snew es(t))):(this.tracksBuffered=[],this.levels=e.map((t=>{const e=new es(t);return this.tracksBuffered[e.id]=[],e})),this.fragmentTracker.removeFragmentsInRange(0,Number.POSITIVE_INFINITY,_e),this.fragPrevious=null,this.mediaBuffer=null)}onSubtitleTrackSwitch(t,e){var s;if(this.currentTrackId=e.id,null==(s=this.levels)||!s.length||-1===this.currentTrackId)return void this.clearInterval();const i=this.levels[this.currentTrackId];null!=i&&i.details?this.mediaBuffer=this.mediaBufferTimeRanges:this.mediaBuffer=null,i&&this.setInterval(500)}onSubtitleTrackLoaded(t,e){var s;const{currentTrackId:i,levels:r}=this,{details:n,id:a}=e;if(!r)return void this.warn(`Subtitle tracks were reset while loading level ${a}`);const o=r[a];if(a>=r.length||!o)return;this.log(`Subtitle track ${a} loaded [${n.startSN},${n.endSN}]${n.lastPartSn?`[part-${n.lastPartSn}-${n.lastPartIndex}]`:""},duration:${n.totalduration}`),this.mediaBuffer=this.mediaBufferTimeRanges;let l=0;if(n.live||null!=(s=o.details)&&s.live){const t=this.mainDetails;if(n.deltaUpdateFailed||!t)return;const e=t.fragments[0];var h;if(o.details)l=this.alignPlaylists(n,o.details,null==(h=this.levelLastLoaded)?void 0:h.details),0===l&&e&&(l=e.start,os(n,l));else n.hasProgramDateTime&&t.hasProgramDateTime?(ei(n,t),l=n.fragments[0].start):e&&(l=e.start,os(n,l))}if(o.details=n,this.levelLastLoaded=o,a===i&&(this.startFragRequested||!this.mainDetails&&n.live||this.setStartPosition(this.mainDetails||n,l),this.tick(),n.live&&!this.fragCurrent&&this.media&&this.state===fi)){vs(null,n.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0)}}_handleFragmentLoadComplete(t){const{frag:e,payload:s}=t,i=e.decryptdata,r=this.hls;if(!this.fragContextChanged(e)&&s&&s.byteLength>0&&null!=i&&i.key&&i.iv&&"AES-128"===i.method){const t=performance.now();this.decrypter.decrypt(new Uint8Array(s),i.key.buffer,i.iv.buffer).catch((t=>{throw r.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:e}),t})).then((s=>{const i=performance.now();r.trigger(p.FRAG_DECRYPTED,{frag:e,payload:s,stats:{tstart:t,tdecrypt:i}})})).catch((t=>{this.warn(`${t.name}: ${t.message}`),this.state=fi}))}}doTick(){if(this.media){if(this.state===fi){const{currentTrackId:t,levels:e}=this,s=null==e?void 0:e[t];if(!s||!e.length||!s.details)return;const{config:i}=this,r=this.getLoadPosition(),n=Xs.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],r,i.maxBufferHole),{end:a,len:o}=n,l=this.getFwdBufferInfo(this.media,De),h=s.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+h.levelTargetDuration)return;const d=h.fragments,c=d.length,u=h.edge;let f=null;const g=this.fragPrevious;if(au-t?0:t;f=vs(g,d,Math.max(d[0].start,a),e),!f&&g&&g.startthis.pollTrackChange(0),this.useTextTrackPolling=!1,this.subtitlePollingInterval=-1,this._subtitleDisplay=!0,this.onTextTracksChanged=()=>{if(this.useTextTrackPolling||self.clearInterval(this.subtitlePollingInterval),!this.media||!this.hls.config.renderTextTracksNatively)return;let t=null;const e=Ue(this.media.textTracks);for(let s=0;s-1&&this.toggleTrackModes()}registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(p.ERROR,this.onError,this)}onMediaAttached(t,e){this.media=e.media,this.media&&(this.queuedDefaultTrack>-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))}pollTrackChange(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)}onMediaDetaching(){if(!this.media)return;self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId);Ue(this.media.textTracks).forEach((t=>{Oe(t)})),this.subtitleTrack=-1,this.media=null}onManifestLoading(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0}onManifestParsed(t,e){this.tracks=e.subtitleTracks}onSubtitleTrackLoaded(t,e){const{id:s,groupId:i,details:r}=e,n=this.tracksInGroup[s];if(!n||n.groupId!==i)return void this.warn(`Subtitle track with id:${s} and group:${i} not found in active group ${null==n?void 0:n.groupId}`);const a=n.details;n.details=e.details,this.log(`Subtitle track ${s} "${n.name}" lang:${n.lang} group:${i} loaded [${r.startSN}-${r.endSN}]`),s===this.trackId&&this.playlistLoaded(s,e,a)}onLevelLoading(t,e){this.switchLevel(e.level)}onLevelSwitching(t,e){this.switchLevel(e.level)}switchLevel(t){const e=this.hls.levels[t];if(!e)return;const s=e.subtitleGroups||null,i=this.groupIds;let r=this.currentTrack;if(!s||(null==i?void 0:i.length)!==(null==s?void 0:s.length)||null!=s&&s.some((t=>-1===(null==i?void 0:i.indexOf(t))))){this.groupIds=s,this.trackId=-1,this.currentTrack=null;const t=this.tracks.filter((t=>!s||-1!==s.indexOf(t.groupId)));if(t.length)this.selectDefaultTrack&&!t.some((t=>t.default))&&(this.selectDefaultTrack=!1),t.forEach(((t,e)=>{t.id=e}));else if(!r&&!this.tracksInGroup.length)return;this.tracksInGroup=t;const e=this.hls.config.subtitlePreference;if(!r&&e){this.selectDefaultTrack=!1;const s=Os(e,t);if(s>-1)r=t[s];else{const t=Os(e,this.tracks);r=this.tracks[t]}}let i=this.findTrackId(r);-1===i&&r&&(i=this.findTrackId(null));const n={subtitleTracks:t};this.log(`Updating subtitle tracks, ${t.length} track(s) found in "${null==s?void 0:s.join(",")}" group-id`),this.hls.trigger(p.SUBTITLE_TRACKS_UPDATED,n),-1!==i&&-1===this.trackId&&this.setSubtitleTrack(i)}else this.shouldReloadPlaylist(r)&&this.setSubtitleTrack(this.trackId)}findTrackId(t){const e=this.tracksInGroup,s=this.selectDefaultTrack;for(let i=0;i-1){const t=this.tracksInGroup[i];return this.setSubtitleTrack(i),t}if(s)return null;{const s=Os(t,e);if(s>-1)return e[s]}}}return null}loadPlaylist(t){super.loadPlaylist();const e=this.currentTrack;if(this.shouldLoadPlaylist(e)&&e){const s=e.id,i=e.groupId;let r=e.url;if(t)try{r=t.addDirectives(r)}catch(t){this.warn(`Could not construct new URL with HLS Delivery Directives: ${t}`)}this.log(`Loading subtitle playlist for id ${s}`),this.hls.trigger(p.SUBTITLE_TRACK_LOADING,{url:r,id:s,groupId:i,deliveryDirectives:t||null})}}toggleTrackModes(){const{media:t}=this;if(!t)return;const e=Ue(t.textTracks),s=this.currentTrack;let i;if(s&&(i=e.filter((t=>Mr(s,t)))[0],i||this.warn(`Unable to find subtitle TextTrack with name "${s.name}" and language "${s.lang}"`)),[].slice.call(e).forEach((t=>{"disabled"!==t.mode&&t!==i&&(t.mode="disabled")})),i){const t=this.subtitleDisplay?"showing":"hidden";i.mode!==t&&(i.mode=t)}}setSubtitleTrack(t){const e=this.tracksInGroup;if(!this.media)return void(this.queuedDefaultTrack=t);if(t<-1||t>=e.length||!f(t))return void this.warn(`Invalid subtitle track id: ${t}`);this.clearTimer(),this.selectDefaultTrack=!1;const s=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),!i)return void this.hls.trigger(p.SUBTITLE_TRACK_SWITCH,{id:t});const r=!!i.details&&!i.details.live;if(t===this.trackId&&i===s&&r)return;this.log(`Switching to subtitle-track ${t}`+(i?` "${i.name}" lang:${i.lang} group:${i.groupId}`:""));const{id:n,groupId:a="",name:o,type:l,url:h}=i;this.hls.trigger(p.SUBTITLE_TRACK_SWITCH,{id:n,groupId:a,name:o,type:l,url:h});const d=this.switchParams(i.url,null==s?void 0:s.details,i.details);this.loadPlaylist(d)}},timelineController:class{constructor(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(p.FRAG_LOADING,this.onFragLoading,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this),t.on(p.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(p.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(p.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(p.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this)}destroy(){const{hls:t}=this;t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(p.FRAG_LOADING,this.onFragLoading,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this),t.off(p.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(p.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(p.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(p.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0}initCea608Parsers(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){const t=new nn(this,"textTrack1"),e=new nn(this,"textTrack2"),s=new nn(this,"textTrack3"),i=new nn(this,"textTrack4");this.cea608Parser1=new en(1,t,e),this.cea608Parser2=new en(3,s,i)}}addCues(t,e,s,i,r){let n=!1;for(let t=r.length;t--;){const i=r[t],d=(a=i[0],o=i[1],l=e,h=s,Math.min(o,h)-Math.max(a,l));if(d>=0&&(i[0]=Math.min(i[0],e),i[1]=Math.max(i[1],s),n=!0,d/(s-e)>.5))return}var a,o,l,h;if(n||r.push([e,s]),this.config.renderTextTracksNatively){const r=this.captionsTracks[t];this.Cues.newCue(r,e,s,i)}else{const r=this.Cues.newCue(null,e,s,i);this.hls.trigger(p.CUES_PARSED,{type:"captions",cues:r,track:t})}}onInitPtsFound(t,{frag:e,id:s,initPTS:i,timescale:r}){const{unparsedVttFrags:n}=this;"main"===s&&(this.initPTS[e.cc]={baseTime:i,timescale:r}),n.length&&(this.unparsedVttFrags=[],n.forEach((t=>{this.onFragLoaded(p.FRAG_LOADED,t)})))}getExistingTrack(t,e){const{media:s}=this;if(s)for(let i=0;i{Oe(t[e]),delete t[e]})),this.nonNativeCaptionsTracks={}}onManifestLoading(){this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this._cleanTracks(),this.tracks=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.textTracks=[],this.unparsedVttFrags=[],this.initPTS=[],this.cea608Parser1&&this.cea608Parser2&&(this.cea608Parser1.reset(),this.cea608Parser2.reset())}_cleanTracks(){const{media:t}=this;if(!t)return;const e=t.textTracks;if(e)for(let t=0;tt.textCodec===Sn));if(this.config.enableWebVTT||i&&this.config.enableIMSC1){if(xr(this.tracks,s))return void(this.tracks=s);if(this.textTracks=[],this.tracks=s,this.config.renderTextTracksNatively){const t=this.media,e=t?Ue(t.textTracks):null;if(this.tracks.forEach(((t,s)=>{let i;if(e){let s=null;for(let i=0;inull!==t)).map((t=>t.label));t.length&&A.warn(`Media element contains unused subtitle tracks: ${t.join(", ")}. Replace media element for each source to clear TextTracks and captions menu.`)}}else if(this.tracks.length){const t=this.tracks.map((t=>({label:t.name,kind:t.type.toLowerCase(),default:t.default,subtitleTrack:t})));this.hls.trigger(p.NON_NATIVE_TEXT_TRACKS_FOUND,{tracks:t})}}}onManifestLoaded(t,e){this.config.enableCEA708Captions&&e.captions&&e.captions.forEach((t=>{const e=/(?:CC|SERVICE)([1-4])/.exec(t.instreamId);if(!e)return;const s=`textTrack${e[1]}`,i=this.captionsProperties[s];i&&(i.label=t.name,t.lang&&(i.languageCode=t.lang),i.media=t)}))}closedCaptionsForLevel(t){const e=this.hls.levels[t.level];return null==e?void 0:e.attrs["CLOSED-CAPTIONS"]}onFragLoading(t,e){if(this.enabled&&e.frag.type===De){var s,i;const{cea608Parser1:t,cea608Parser2:r,lastSn:n}=this,{cc:a,sn:o}=e.frag,l=null!=(s=null==(i=e.part)?void 0:i.index)?s:-1;t&&r&&(o!==n+1||o===n&&l!==this.lastPartIndex+1||a!==this.lastCc)&&(t.reset(),r.reset()),this.lastCc=a,this.lastSn=o,this.lastPartIndex=l}}onFragLoaded(t,e){const{frag:s,payload:i}=e;if(s.type===_e)if(i.byteLength){const t=s.decryptdata,r="stats"in e;if(null==t||!t.encrypted||r){const t=this.tracks[s.level],r=this.vttCCs;r[s.cc]||(r[s.cc]={start:s.start,prevCC:this.prevCC,new:!0},this.prevCC=s.cc),t&&t.textCodec===Sn?this._parseIMSC1(s,i):this._parseVTTs(e)}}else this.hls.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:s,error:new Error("Empty subtitle payload")})}_parseIMSC1(t,e){const s=this.hls;bn(e,this.initPTS[t.cc],(e=>{this._appendCues(e,t.level),s.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!0,frag:t})}),(e=>{A.log(`Failed to parse IMSC1: ${e}`),s.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:t,error:e})}))}_parseVTTs(t){var e;const{frag:s,payload:i}=t,{initPTS:r,unparsedVttFrags:n}=this,a=r.length-1;if(!r[s.cc]&&-1===a)return void n.push(t);const o=this.hls;Tn(null!=(e=s.initSegment)&&e.data?Bt(s.initSegment.data,new Uint8Array(i)):i,this.initPTS[s.cc],this.vttCCs,s.cc,s.start,(t=>{this._appendCues(t,s.level),o.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!0,frag:s})}),(e=>{const r="Missing initPTS for VTT MPEGTS"===e.message;r?n.push(t):this._fallbackToIMSC1(s,i),A.log(`Failed to parse VTT cue: ${e}`),r&&a>s.cc||o.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:s,error:e})}))}_fallbackToIMSC1(t,e){const s=this.tracks[t.level];s.textCodec||bn(e,this.initPTS[t.cc],(()=>{s.textCodec=Sn,this._parseIMSC1(t,e)}),(()=>{s.textCodec="wvtt"}))}_appendCues(t,e){const s=this.hls;if(this.config.renderTextTracksNatively){const s=this.textTracks[e];if(!s||"disabled"===s.mode)return;t.forEach((t=>Fe(s,t)))}else{const i=this.tracks[e];if(!i)return;const r=i.default?"default":"subtitles"+e;s.trigger(p.CUES_PARSED,{type:"subtitles",cues:t,track:r})}}onFragDecrypted(t,e){const{frag:s}=e;s.type===_e&&this.onFragLoaded(p.FRAG_LOADED,e)}onSubtitleTracksCleared(){this.tracks=[],this.captionsTracks={}}onFragParsingUserdata(t,e){this.initCea608Parsers();const{cea608Parser1:s,cea608Parser2:i}=this;if(!this.enabled||!s||!i)return;const{frag:r,samples:n}=e;if(r.type!==De||"NONE"!==this.closedCaptionsForLevel(r))for(let t=0;tNe(t[i],e,s)))}if(this.config.renderTextTracksNatively&&0===e&&void 0!==i){const{textTracks:t}=this;Object.keys(t).forEach((s=>Ne(t[s],e,i)))}}}extractCea608Data(t){const e=[[],[]],s=31&t[0];let i=2;for(let r=0;r0&&-1===t?(this.log(`Override startPosition with lastCurrentTime @${e.toFixed(3)}`),t=e,this.state=fi):(this.loadedmetadata=!1,this.state=vi),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}doTick(){switch(this.state){case fi:this.doTickIdle();break;case vi:{var t;const{levels:e,trackId:s}=this,i=null==e||null==(t=e[s])?void 0:t.details;if(i){if(this.waitForCdnTuneIn(i))break;this.state=Li}break}case pi:{var e;const t=performance.now(),s=this.retryDate;if(!s||t>=s||null!=(e=this.media)&&e.seeking){const{levels:t,trackId:e}=this;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==t?void 0:t[e])||null),this.state=fi}break}case Li:{const t=this.waitingData;if(t){const{frag:e,part:s,cache:i,complete:r}=t;if(void 0!==this.initPTS[e.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=mi;const t={frag:e,part:s,payload:i.flush(),networkDetails:null};this._handleFragmentLoadProgress(t),r&&super._handleFragmentLoadComplete(t)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log(`Waiting fragment cc (${e.cc}) cancelled because video is at cc ${this.videoTrackCC}`),this.clearWaitingFragment();else{const t=this.getLoadPosition(),s=Xs.bufferInfo(this.mediaBuffer,t,this.config.maxBufferHole);ys(s.end,this.config.maxFragLookUpTolerance,e)<0&&(this.log(`Waiting fragment cc (${e.cc}) @ ${e.start} cancelled because another fragment at ${s.end} is needed`),this.clearWaitingFragment())}}else this.state=fi}}this.onTickEnd()}clearWaitingFragment(){const t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=fi)}resetLoadingState(){this.clearWaitingFragment(),super.resetLoadingState()}onTickEnd(){const{media:t}=this;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)}doTickIdle(){const{hls:t,levels:e,media:s,trackId:i}=this,r=t.config;if(!s&&(this.startFragRequested||!r.startFragPrefetch)||null==e||!e[i])return;const n=e[i],a=n.details;if(!a||a.live&&this.levelLastLoaded!==n||this.waitForCdnTuneIn(a))return void(this.state=vi);const o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,_,Ie));const l=this.getFwdBufferInfo(o,Ie);if(null===l)return;const{bufferedTrack:h,switchingTrack:d}=this;if(!d&&this._streamEnded(l,a))return t.trigger(p.BUFFER_EOS,{type:"audio"}),void(this.state=Ti);const c=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,De),u=l.len,f=this.getMaxBufferLength(null==c?void 0:c.len),g=a.fragments,m=g[0].start;let v=this.flushing?this.getLoadPosition():l.end;if(d&&s){const t=this.getLoadPosition();h&&!Pr(d.attrs,h.attrs)&&(v=t),a.PTSKnown&&tm||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),s.currentTime=m+.05)}if(u>=f&&!d&&vc.end+a.targetduration;if(T||(null==c||!c.len)&&l.len){const t=this.getAppendedFrag(y.start,De);if(null===t)return;if(E||(E=!!t.gap||!!T&&0===c.len),T&&!E||E&&l.nextStart&&l.nextStartnew es(t)))}onAudioTrackSwitching(t,e){const s=!!e.url;this.trackId=e.id;const{fragCurrent:i}=this;i&&(i.abortRequests(),this.removeUnbufferedFrags(i.start)),this.resetLoadingState(),s?this.setInterval(100):this.resetTransmuxer(),s?(this.switchingTrack=e,this.state=fi,this.flushAudioIfNeeded(e)):(this.switchingTrack=null,this.bufferedTrack=e,this.state=ui),this.tick()}onManifestLoading(){this.fragmentTracker.removeAllFragments(),this.startPosition=this.lastCurrentTime=0,this.bufferFlushed=this.flushing=!1,this.levels=this.mainDetails=this.waitingData=this.bufferedTrack=this.cachedTrackLoadedData=this.switchingTrack=null,this.startFragRequested=!1,this.trackId=this.videoTrackCC=this.waitingVideoCC=-1}onLevelLoaded(t,e){this.mainDetails=e.details,null!==this.cachedTrackLoadedData&&(this.hls.trigger(p.AUDIO_TRACK_LOADED,this.cachedTrackLoadedData),this.cachedTrackLoadedData=null)}onAudioTrackLoaded(t,e){var s;if(null==this.mainDetails)return void(this.cachedTrackLoadedData=e);const{levels:i}=this,{details:r,id:n}=e;if(!i)return void this.warn(`Audio tracks were reset while loading level ${n}`);this.log(`Audio track ${n} loaded [${r.startSN},${r.endSN}]${r.lastPartSn?`[part-${r.lastPartSn}-${r.lastPartIndex}]`:""},duration:${r.totalduration}`);const a=i[n];let o=0;if(r.live||null!=(s=a.details)&&s.live){this.checkLiveUpdate(r);const t=this.mainDetails;if(r.deltaUpdateFailed||!t)return;var l;if(!a.details&&r.hasProgramDateTime&&t.hasProgramDateTime)ei(r,t),o=r.fragments[0].start;else o=this.alignPlaylists(r,a.details,null==(l=this.levelLastLoaded)?void 0:l.details)}a.details=r,this.levelLastLoaded=a,this.startFragRequested||!this.mainDetails&&r.live||this.setStartPosition(this.mainDetails||r,o),this.state!==vi||this.waitForCdnTuneIn(r)||(this.state=fi),this.tick()}_handleFragmentLoadProgress(t){var e;const{frag:s,part:i,payload:r}=t,{config:n,trackId:a,levels:o}=this;if(!o)return void this.warn(`Audio tracks were reset while fragment load was in progress. Fragment ${s.sn} of level ${s.level} will not be buffered`);const l=o[a];if(!l)return void this.warn("Audio track is undefined on fragment load progress");const h=l.details;if(!h)return this.warn("Audio track details undefined on fragment load progress"),void this.removeUnbufferedFrags(s.start);const d=n.defaultAudioCodec||l.audioCodec||"mp4a.40.2";let c=this.transmuxer;c||(c=this.transmuxer=new Cr(this.hls,Ie,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)));const u=this.initPTS[s.cc],f=null==(e=s.initSegment)?void 0:e.data;if(void 0!==u){const t=!1,e=i?i.index:-1,n=-1!==e,a=new zs(s.level,s.sn,s.stats.chunkCount,r.byteLength,e,n);c.push(r,f,d,"",s,i,h.totalduration,t,a,u)}else{this.log(`Unknown video PTS for cc ${s.cc}, waiting for video PTS before demuxing audio frag ${s.sn} of [${h.startSN} ,${h.endSN}],track ${a}`);const{cache:t}=this.waitingData=this.waitingData||{frag:s,part:i,cache:new bi,complete:!1};t.push(new Uint8Array(r)),this.waitingVideoCC=this.videoTrackCC,this.state=Li}}_handleFragmentLoadComplete(t){this.waitingData?this.waitingData.complete=!0:super._handleFragmentLoadComplete(t)}onBufferReset(){this.mediaBuffer=this.videoBuffer=null,this.loadedmetadata=!1}onBufferCreated(t,e){const s=e.tracks.audio;s&&(this.mediaBuffer=s.buffer||null),e.tracks.video&&(this.videoBuffer=e.tracks.video.buffer||null)}onFragBuffered(t,e){const{frag:s,part:i}=e;if(s.type===Ie)if(this.fragContextChanged(s))this.warn(`Fragment ${s.sn}${i?" p: "+i.index:""} of level ${s.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.switchingTrack?this.switchingTrack.name:"false"}`);else{if("initSegment"!==s.sn){this.fragPrevious=s;const t=this.switchingTrack;t&&(this.bufferedTrack=t,this.switchingTrack=null,this.hls.trigger(p.AUDIO_TRACK_SWITCHED,h({},t)))}this.fragBufferedComplete(s,i)}else if(!this.loadedmetadata&&s.type===De){const t=this.videoBuffer||this.media;if(t){Xs.getBuffered(t).length&&(this.loadedmetadata=!0)}}}onError(t,e){var s;if(e.fatal)this.state=Si;else switch(e.details){case y.FRAG_GAP:case y.FRAG_PARSING_ERROR:case y.FRAG_DECRYPT_ERROR:case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(Ie,e);break;case y.AUDIO_TRACK_LOAD_ERROR:case y.AUDIO_TRACK_LOAD_TIMEOUT:case y.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==vi||(null==(s=e.context)?void 0:s.type)!==ke||(this.state=fi);break;case y.BUFFER_APPEND_ERROR:case y.BUFFER_FULL_ERROR:if(!e.parent||"audio"!==e.parent)return;if(e.details===y.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&(this.bufferedTrack=null,super.flushMainBuffer(0,Number.POSITIVE_INFINITY,"audio"));break;case y.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}}onBufferFlushing(t,{type:e}){e!==C&&(this.flushing=!0)}onBufferFlushed(t,{type:e}){if(e!==C){this.flushing=!1,this.bufferFlushed=!0,this.state===Ti&&(this.state=fi);const t=this.mediaBuffer||this.media;t&&(this.afterBufferFlushed(t,e,Ie),this.tick())}}_handleTransmuxComplete(t){var e;const s="audio",{hls:i}=this,{remuxResult:r,chunkMeta:n}=t,a=this.getCurrentContext(n);if(!a)return void this.resetWhenMissingContext(n);const{frag:o,part:l,level:h}=a,{details:d}=h,{audio:c,text:f,id3:g,initSegment:m}=r;if(!this.fragContextChanged(o)&&d){if(this.state=yi,this.switchingTrack&&c&&this.completeAudioSwitch(this.switchingTrack),null!=m&&m.tracks){const t=o.initSegment||o;this._bufferInitSegment(h,m.tracks,t,n),i.trigger(p.FRAG_PARSING_INIT_SEGMENT,{frag:t,id:s,tracks:m.tracks})}if(c){const{startPTS:t,endPTS:e,startDTS:s,endDTS:i}=c;l&&(l.elementaryStreams[_]={startPTS:t,endPTS:e,startDTS:s,endDTS:i}),o.setElementaryStreamInfo(_,t,e,s,i),this.bufferFragmentData(c,o,l,n)}if(null!=g&&null!=(e=g.samples)&&e.length){const t=u({id:s,frag:o,details:d},g);i.trigger(p.FRAG_PARSING_METADATA,t)}if(f){const t=u({id:s,frag:o,details:d},f);i.trigger(p.FRAG_PARSING_USERDATA,t)}}else this.fragmentTracker.removeFragment(o)}_bufferInitSegment(t,e,s,i){if(this.state!==yi)return;e.video&&delete e.video;const r=e.audio;if(!r)return;r.id="audio";const n=t.audioCodec;this.log(`Init audio buffer, container:${r.container}, codecs[level/parsed]=[${n}/${r.codec}]`),n&&1===n.split(",").length&&(r.levelCodec=n),this.hls.trigger(p.BUFFER_CODECS,e);const a=r.initSegment;if(null!=a&&a.byteLength){const t={type:"audio",frag:s,part:null,chunkMeta:i,parent:s.type,data:a};this.hls.trigger(p.BUFFER_APPENDING,t)}this.tickImmediate()}loadFragment(t,e,s){const i=this.fragmentTracker.getState(t);var r;if(this.fragCurrent=t,this.switchingTrack||i===Gs||i===Hs)if("initSegment"===t.sn)this._loadInitSegment(t,e);else if(null!=(r=e.details)&&r.live&&!this.initPTS[t.cc]){this.log(`Waiting for video PTS in continuity counter ${t.cc} of live stream before loading audio fragment ${t.sn} of level ${this.trackId}`),this.state=Li;const s=this.mainDetails;s&&s.fragments[0].start!==e.details.fragments[0].start&&ei(e.details,s)}else this.startFragRequested=!0,super.loadFragment(t,e,s);else this.clearTrackerIfNeeded(t)}flushAudioIfNeeded(t){const{media:e,bufferedTrack:s}=this,i=null==s?void 0:s.attrs,r=t.attrs;e&&i&&(i.CHANNELS!==r.CHANNELS||s.name!==t.name||s.lang!==t.lang)&&(this.log("Switching audio track : flushing all audio"),super.flushMainBuffer(0,Number.POSITIVE_INFINITY,"audio"),this.bufferedTrack=null)}completeAudioSwitch(t){const{hls:e}=this;this.flushAudioIfNeeded(t),this.bufferedTrack=t,this.switchingTrack=null,e.trigger(p.AUDIO_TRACK_SWITCHED,h({},t))}},audioTrackController:class extends ws{constructor(t){super(t,"[audio-track-controller]"),this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0,this.registerListeners()}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.off(p.ERROR,this.onError,this)}destroy(){this.unregisterListeners(),this.tracks.length=0,this.tracksInGroup.length=0,this.currentTrack=null,super.destroy()}onManifestLoading(){this.tracks=[],this.tracksInGroup=[],this.groupIds=null,this.currentTrack=null,this.trackId=-1,this.selectDefaultTrack=!0}onManifestParsed(t,e){this.tracks=e.audioTracks||[]}onAudioTrackLoaded(t,e){const{id:s,groupId:i,details:r}=e,n=this.tracksInGroup[s];if(!n||n.groupId!==i)return void this.warn(`Audio track with id:${s} and group:${i} not found in active group ${null==n?void 0:n.groupId}`);const a=n.details;n.details=e.details,this.log(`Audio track ${s} "${n.name}" lang:${n.lang} group:${i} loaded [${r.startSN}-${r.endSN}]`),s===this.trackId&&this.playlistLoaded(s,e,a)}onLevelLoading(t,e){this.switchLevel(e.level)}onLevelSwitching(t,e){this.switchLevel(e.level)}switchLevel(t){const e=this.hls.levels[t];if(!e)return;const s=e.audioGroups||null,i=this.groupIds;let r=this.currentTrack;if(!s||(null==i?void 0:i.length)!==(null==s?void 0:s.length)||null!=s&&s.some((t=>-1===(null==i?void 0:i.indexOf(t))))){this.groupIds=s,this.trackId=-1,this.currentTrack=null;const t=this.tracks.filter((t=>!s||-1!==s.indexOf(t.groupId)));if(t.length)this.selectDefaultTrack&&!t.some((t=>t.default))&&(this.selectDefaultTrack=!1),t.forEach(((t,e)=>{t.id=e}));else if(!r&&!this.tracksInGroup.length)return;this.tracksInGroup=t;const e=this.hls.config.audioPreference;if(!r&&e){const s=Os(e,t,Us);if(s>-1)r=t[s];else{const t=Os(e,this.tracks);r=this.tracks[t]}}let i=this.findTrackId(r);-1===i&&r&&(i=this.findTrackId(null));const a={audioTracks:t};this.log(`Updating audio tracks, ${t.length} track(s) found in group(s): ${null==s?void 0:s.join(",")}`),this.hls.trigger(p.AUDIO_TRACKS_UPDATED,a);const o=this.trackId;if(-1!==i&&-1===o)this.setAudioTrack(i);else if(t.length&&-1===o){var n;const e=new Error(`No audio track selected for current audio group-ID(s): ${null==(n=this.groupIds)?void 0:n.join(",")} track count: ${t.length}`);this.warn(e.message),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:e})}}else this.shouldReloadPlaylist(r)&&this.setAudioTrack(this.trackId)}onError(t,e){!e.fatal&&e.context&&(e.context.type!==ke||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))}get allAudioTracks(){return this.tracks}get audioTracks(){return this.tracksInGroup}get audioTrack(){return this.trackId}set audioTrack(t){this.selectDefaultTrack=!1,this.setAudioTrack(t)}setAudioOption(t){const e=this.hls;if(e.config.audioPreference=t,t){const s=this.allAudioTracks;if(this.selectDefaultTrack=!1,s.length){const i=this.currentTrack;if(i&&Ns(t,i,Us))return i;const r=Os(t,this.tracksInGroup,Us);if(r>-1){const t=this.tracksInGroup[r];return this.setAudioTrack(r),t}if(i){let i=e.loadLevel;-1===i&&(i=e.firstAutoLevel);const r=function(t,e,s,i,r){const n=e[i],a=e.reduce(((t,e,s)=>{const i=e.uri;return(t[i]||(t[i]=[])).push(s),t}),{})[n.uri];a.length>1&&(i=Math.max.apply(Math,a));const o=n.videoRange,l=n.frameRate,h=n.codecSet.substring(0,4),d=Bs(e,i,(e=>{if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==h)return!1;const i=e.audioGroups,n=s.filter((t=>!i||-1!==i.indexOf(t.groupId)));return Os(t,n,r)>-1}));return d>-1?d:Bs(e,i,(e=>{const i=e.audioGroups,n=s.filter((t=>!i||-1!==i.indexOf(t.groupId)));return Os(t,n,r)>-1}))}(t,e.levels,s,i,Us);if(-1===r)return null;e.nextLoadLevel=r}if(t.channels||t.audioCodec){const e=Os(t,s);if(e>-1)return s[e]}}}return null}setAudioTrack(t){const e=this.tracksInGroup;if(t<0||t>=e.length)return void this.warn(`Invalid audio track id: ${t}`);this.clearTimer(),this.selectDefaultTrack=!1;const s=this.currentTrack,i=e[t],r=i.details&&!i.details.live;if(t===this.trackId&&i===s&&r)return;if(this.log(`Switching to audio-track ${t} "${i.name}" lang:${i.lang} group:${i.groupId} channels:${i.channels}`),this.trackId=t,this.currentTrack=i,this.hls.trigger(p.AUDIO_TRACK_SWITCHING,h({},i)),r)return;const n=this.switchParams(i.url,null==s?void 0:s.details,i.details);this.loadPlaylist(n)}findTrackId(t){const e=this.tracksInGroup;for(let s=0;s{this.initialized&&(this.starved=!0),this.buffering=!0},this.onPlaying=()=>{this.initialized||(this.initialized=!0),this.buffering=!1},this.applyPlaylistData=t=>{try{this.apply(t,{ot:Un.MANIFEST,su:!this.initialized})}catch(t){A.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=t=>{try{const e=t.frag,s=this.hls.levels[e.level],i=this.getObjectType(e),r={d:1e3*e.duration,ot:i};i!==Un.VIDEO&&i!==Un.AUDIO&&i!=Un.MUXED||(r.br=s.bitrate/1e3,r.tb=this.getTopBandwidth(i)/1e3,r.bl=this.getBufferLength(i)),this.apply(t,r)}catch(t){A.warn("Could not generate segment CMCD data.",t)}},this.hls=t;const e=this.config=t.config,{cmcd:s}=e;null!=s&&(e.pLoader=this.createPlaylistLoader(),e.fLoader=this.createFragmentLoader(),this.sid=s.sessionId||function(){try{return crypto.randomUUID()}catch(t){try{const t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){let e=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(t=>{const s=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"==t?s:3&s|8).toString(16)}))}}}(),this.cid=s.contentId,this.useHeaders=!0===s.useHeaders,this.includeKeys=s.includeKeys,this.registerListeners())}registerListeners(){const t=this.hls;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHED,this.onMediaDetached,this),t.on(p.BUFFER_CREATED,this.onBufferCreated,this)}unregisterListeners(){const t=this.hls;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHED,this.onMediaDetached,this),t.off(p.BUFFER_CREATED,this.onBufferCreated,this)}destroy(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null}onMediaAttached(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)}onMediaDetached(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)}onBufferCreated(t,e){var s,i;this.audioBuffer=null==(s=e.tracks.audio)?void 0:s.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer}createData(){var t;return{v:1,sf:Bn.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}}apply(t,e={}){u(e,this.createData());const s=e.ot===Un.INIT||e.ot===Un.VIDEO||e.ot===Un.MUXED;this.starved&&s&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);const{includeKeys:i}=this;i&&(e=Object.keys(e).reduce(((t,s)=>(i.includes(s)&&(t[s]=e[s]),t)),{})),this.useHeaders?(t.headers||(t.headers={}),ha(t.headers,e)):t.url=ca(t.url,e)}getObjectType(t){const{type:e}=t;return"subtitle"===e?Un.TIMED_TEXT:"initSegment"===t.sn?Un.INIT:"audio"===e?Un.AUDIO:"main"===e?this.hls.audioTracks.length?Un.VIDEO:Un.MUXED:void 0}getTopBandwidth(t){let e,s=0;const i=this.hls;if(t===Un.AUDIO)e=i.audioTracks;else{const t=i.maxAutoLevel,s=t>-1?t+1:i.levels.length;e=i.levels.slice(0,s)}for(const t of e)t.bitrate>s&&(s=t.bitrate);return s>0?s:NaN}getBufferLength(t){const e=this.hls.media,s=t===Un.AUDIO?this.audioBuffer:this.videoBuffer;if(!s||!e)return NaN;return 1e3*Xs.bufferInfo(s,e.currentTime,this.config.maxBufferHole).len}createPlaylistLoader(){const{pLoader:t}=this.config,e=this.applyPlaylistData,s=t||this.config.loader;return class{constructor(t){this.loader=void 0,this.loader=new s(t)}get stats(){return this.loader.stats}get context(){return this.loader.context}destroy(){this.loader.destroy()}abort(){this.loader.abort()}load(t,s,i){e(t),this.loader.load(t,s,i)}}}createFragmentLoader(){const{fLoader:t}=this.config,e=this.applyFragmentData,s=t||this.config.loader;return class{constructor(t){this.loader=void 0,this.loader=new s(t)}get stats(){return this.loader.stats}get context(){return this.loader.context}destroy(){this.loader.destroy()}abort(){this.loader.abort()}load(t,s,i){e(t),this.loader.load(t,s,i)}}}},contentSteeringController:class{constructor(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=A.log.bind(A,"[content-steering]:"),this.registerListeners()}registerListeners(){const t=this.hls;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const t=this.hls;t&&(t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.ERROR,this.onError,this))}startLoad(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){const t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}}stopLoad(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()}clearTimeout(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)}destroy(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null}removeLevel(t){const e=this.levels;e&&(this.levels=e.filter((e=>e!==t)))}onManifestLoading(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null}onManifestLoaded(t,e){const{contentSteering:s}=e;null!==s&&(this.pathwayId=s.pathwayId,this.uri=s.uri,this.started&&this.startLoad())}onManifestParsed(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks}onError(t,e){const{errorAction:s}=e;if((null==s?void 0:s.action)===Ss&&s.flags===bs){const t=this.levels;let i=this.pathwayPriority,r=this.pathwayId;if(e.context){const{groupId:s,pathwayId:i,type:n}=e.context;s&&t?r=this.getPathwayForGroupId(s,n,r):i&&(r=i)}r in this.penalizedPathways||(this.penalizedPathways[r]=performance.now()),!i&&t&&(i=t.reduce(((t,e)=>(-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t)),[])),i&&i.length>1&&(this.updatePathwayPriority(i),s.resolved=this.pathwayId!==r),s.resolved||A.warn(`Could not resolve ${e.details} ("${e.error.message}") with content-steering for Pathway: ${r} levels: ${t?t.length:t} priorities: ${JSON.stringify(i)} penalized: ${JSON.stringify(this.penalizedPathways)}`)}}filterParsedLevels(t){this.levels=t;let e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){const s=t[0].pathwayId;this.log(`No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${s}"`),e=this.getLevelsForPathway(s),this.pathwayId=s}return e.length!==t.length?(this.log(`Found ${e.length}/${t.length} levels in Pathway "${this.pathwayId}"`),e):t}getLevelsForPathway(t){return null===this.levels?[]:this.levels.filter((e=>t===e.pathwayId))}updatePathwayPriority(t){let e;this.pathwayPriority=t;const s=this.penalizedPathways,i=performance.now();Object.keys(s).forEach((t=>{i-s[t]>3e5&&delete s[t]}));for(let i=0;i0){this.log(`Setting Pathway to "${r}"`),this.pathwayId=r,ds(e),this.hls.trigger(p.LEVELS_UPDATED,{levels:e});const t=this.hls.levels[n];a&&t&&this.levels&&(t.attrs["STABLE-VARIANT-ID"]!==a.attrs["STABLE-VARIANT-ID"]&&t.bitrate!==a.bitrate&&this.log(`Unstable Pathways change from bitrate ${a.bitrate} to ${t.bitrate}`),this.hls.nextLoadLevel=n);break}}}getPathwayForGroupId(t,e,s){const i=this.getLevelsForPathway(s).concat(this.levels||[]);for(let s=0;s{const{ID:r,"BASE-ID":n,"URI-REPLACEMENT":a}=t;if(e.some((t=>t.pathwayId===r)))return;const o=this.getLevelsForPathway(n).map((t=>{const e=new k(t.attrs);e["PATHWAY-ID"]=r;const n=e.AUDIO&&`${e.AUDIO}_clone_${r}`,o=e.SUBTITLES&&`${e.SUBTITLES}_clone_${r}`;n&&(s[e.AUDIO]=n,e.AUDIO=n),o&&(i[e.SUBTITLES]=o,e.SUBTITLES=o);const l=fa(t.uri,e["STABLE-VARIANT-ID"],"PER-VARIANT-URIS",a),h=new es({attrs:e,audioCodec:t.audioCodec,bitrate:t.bitrate,height:t.height,name:t.name,url:l,videoCodec:t.videoCodec,width:t.width});if(t.audioGroups)for(let e=1;e{this.log(`Loaded steering manifest: "${i}"`);const n=t.data;if(1!==n.VERSION)return void this.log(`Steering VERSION ${n.VERSION} not supported!`);this.updated=performance.now(),this.timeToLoad=n.TTL;const{"RELOAD-URI":a,"PATHWAY-CLONES":o,"PATHWAY-PRIORITY":l}=n;if(a)try{this.uri=new self.URL(a,i).href}catch(t){return this.enabled=!1,void this.log(`Failed to parse Steering Manifest RELOAD-URI: ${a}`)}this.scheduleRefresh(this.uri||s.url),o&&this.clonePathways(o);const h={steeringManifest:n,url:i.toString()};this.hls.trigger(p.STEERING_MANIFEST_LOADED,h),l&&this.updatePathwayPriority(l)},onError:(t,e,s,i)=>{if(this.log(`Error loading steering manifest: ${t.code} ${t.text} (${e.url})`),this.stopLoad(),410===t.code)return this.enabled=!1,void this.log(`Steering manifest ${e.url} no longer available`);let r=1e3*this.timeToLoad;if(429!==t.code)this.scheduleRefresh(this.uri||e.url,r);else{const t=this.loader;if("function"==typeof(null==t?void 0:t.getResponseHeader)){const e=t.getResponseHeader("Retry-After");e&&(r=1e3*parseFloat(e))}this.log(`Steering manifest ${e.url} rate limited`)}},onTimeout:(t,e,s)=>{this.log(`Timeout loading steering manifest (${e.url})`),this.scheduleRefresh(this.uri||e.url)}};this.log(`Requesting steering manifest: ${i}`),this.loader.load(r,o,l)}scheduleRefresh(t,e=1e3*this.timeToLoad){this.clearTimeout(),this.reloadTimer=self.setTimeout((()=>{var e;const s=null==(e=this.hls)?void 0:e.media;!s||s.ended?this.scheduleRefresh(t,1e3*this.timeToLoad):this.loadSteeringManifest(t)}),e)}}});function Aa(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(Aa):Object.keys(t).reduce(((e,s)=>(e[s]=Aa(t[s]),e)),{}):t}function Ra(t){const e=t.loader;if(e!==va&&e!==ma)A.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1;else{(function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1})()&&(t.loader=va,t.progressive=!0,t.enableSoftwareAES=!0,A.log("[config]: Progressive streaming enabled, using FetchLoader"))}}let ba;class ka extends ws{constructor(t,e){super(t,"[level-controller]"),this._levels=[],this._firstLevel=-1,this._maxAutoLevel=-1,this._startLevel=void 0,this.currentLevel=null,this.currentLevelIndex=-1,this.manualLevelIndex=-1,this.steering=void 0,this.onParsedComplete=void 0,this.steering=e,this._registerListeners()}_registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.ERROR,this.onError,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.ERROR,this.onError,this)}destroy(){this._unregisterListeners(),this.steering=null,this.resetLevels(),super.destroy()}stopLoad(){this._levels.forEach((t=>{t.loadError=0,t.fragmentError=0})),super.stopLoad()}resetLevels(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1}onManifestLoading(t,e){this.resetLevels()}onManifestLoaded(t,e){const s=this.hls.config.preferManagedMediaSource,i=[],r={},n={};let a=!1,o=!1,l=!1;e.levels.forEach((t=>{var e,h;const d=t.attrs;let{audioCodec:c,videoCodec:u}=t;-1!==(null==(e=c)?void 0:e.indexOf("mp4a.40.34"))&&(ba||(ba=/chrome|firefox/i.test(navigator.userAgent)),ba&&(t.audioCodec=c=void 0)),c&&(t.audioCodec=c=he(c,s)),0===(null==(h=u)?void 0:h.indexOf("avc1"))&&(u=t.videoCodec=function(t){const e=t.split(",");for(let t=0;t2){let i=s.shift()+".";i+=parseInt(s.shift()).toString(16),i+=("000"+parseInt(s.shift()).toString(16)).slice(-4),e[t]=i}}return e.join(",")}(u));const{width:f,height:g,unknownCodecs:m}=t;if(a||(a=!(!f||!g)),o||(o=!!u),l||(l=!!c),null!=m&&m.length||c&&!se(c,"audio",s)||u&&!se(u,"video",s))return;const{CODECS:p,"FRAME-RATE":v,"HDCP-LEVEL":y,"PATHWAY-ID":E,RESOLUTION:T,"VIDEO-RANGE":S}=d,L=`${`${E||"."}-`}${t.bitrate}-${T}-${v}-${p}-${S}-${y}`;if(r[L])if(r[L].uri===t.url||t.attrs["PATHWAY-ID"])r[L].addGroupId("audio",d.AUDIO),r[L].addGroupId("text",d.SUBTITLES);else{const e=n[L]+=1;t.attrs["PATHWAY-ID"]=new Array(e+1).join(".");const s=new es(t);r[L]=s,i.push(s)}else{const e=new es(t);r[L]=e,n[L]=1,i.push(e)}})),this.filterAndSortMediaOptions(i,e,a,o,l)}filterAndSortMediaOptions(t,e,s,i,r){let n=[],a=[],o=t;if((s||i)&&r&&(o=o.filter((({videoCodec:t,videoRange:e,width:s,height:i})=>{return(!!t||!(!s||!i))&&(!!(r=e)&&Xe.indexOf(r)>-1);var r}))),0===o.length)return void Promise.resolve().then((()=>{if(this.hls){e.levels.length&&this.warn(`One or more CODECS in variant not supported: ${JSON.stringify(e.levels[0].attrs)}`);const t=new Error("no level with compatible codecs found in manifest");this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}));if(e.audioTracks){const{preferManagedMediaSource:t}=this.hls.config;n=e.audioTracks.filter((e=>!e.audioCodec||se(e.audioCodec,"audio",t))),wa(n)}e.subtitles&&(a=e.subtitles,wa(a));const l=o.slice(0);o.sort(((t,e)=>{if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(s&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return Xe.indexOf(t.videoRange)-Xe.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){const s=ne(t.videoCodec),i=ne(e.videoCodec);if(s!==i)return i-s}if(t.uri===e.uri&&t.codecSet!==e.codecSet){const s=ae(t.codecSet),i=ae(e.codecSet);if(s!==i)return i-s}return t.averageBitrate!==e.averageBitrate?t.averageBitrate-e.averageBitrate:0}));let h=l[0];if(this.steering&&(o=this.steering.filterParsedLevels(o),o.length!==l.length))for(let t=0;ts&&s===La.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=t)}break}const c=r&&!i,u={levels:o,audioTracks:n,subtitleTracks:a,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:r,video:i,altAudio:!c&&n.some((t=>!!t.url))};this.hls.trigger(p.MANIFEST_PARSED,u),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}get levels(){return 0===this._levels.length?null:this._levels}get level(){return this.currentLevelIndex}set level(t){const e=this._levels;if(0===e.length)return;if(t<0||t>=e.length){const s=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:s,reason:s.message}),i)return;t=Math.min(t,e.length-1)}const s=this.currentLevelIndex,i=this.currentLevel,r=i?i.attrs["PATHWAY-ID"]:void 0,n=e[t],a=n.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=n,s===t&&n.details&&i&&r===a)return;this.log(`Switching to level ${t} (${n.height?n.height+"p ":""}${n.videoRange?n.videoRange+" ":""}${n.codecSet?n.codecSet+" ":""}@${n.bitrate})${a?" with Pathway "+a:""} from level ${s}${r?" with Pathway "+r:""}`);const o={level:t,attrs:n.attrs,details:n.details,bitrate:n.bitrate,averageBitrate:n.averageBitrate,maxBitrate:n.maxBitrate,realBitrate:n.realBitrate,width:n.width,height:n.height,codecSet:n.codecSet,audioCodec:n.audioCodec,videoCodec:n.videoCodec,audioGroups:n.audioGroups,subtitleGroups:n.subtitleGroups,loaded:n.loaded,loadError:n.loadError,fragmentError:n.fragmentError,name:n.name,id:n.id,uri:n.uri,url:n.url,urlId:0,audioGroupIds:n.audioGroupIds,textGroupIds:n.textGroupIds};this.hls.trigger(p.LEVEL_SWITCHING,o);const l=n.details;if(!l||l.live){const t=this.switchParams(n.uri,null==i?void 0:i.details,l);this.loadPlaylist(t)}}get manualLevel(){return this.manualLevelIndex}set manualLevel(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}get firstLevel(){return this._firstLevel}set firstLevel(t){this._firstLevel=t}get startLevel(){if(void 0===this._startLevel){const t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel}set startLevel(t){this._startLevel=t}onError(t,e){!e.fatal&&e.context&&e.context.type===be&&e.context.level===this.level&&this.checkRetry(e)}onFragBuffered(t,{frag:e}){if(void 0!==e&&e.type===De){const t=e.elementaryStreams;if(!Object.keys(t).some((e=>!!t[e])))return;const s=this._levels[e.level];null!=s&&s.loadError&&(this.log(`Resetting level error count of ${s.loadError} on frag buffered`),s.loadError=0)}}onLevelLoaded(t,e){var s;const{level:i,details:r}=e,n=this._levels[i];var a;if(!n)return this.warn(`Invalid level index ${i}`),void(null!=(a=e.deliveryDirectives)&&a.skip&&(r.deltaUpdateFailed=!0));i===this.currentLevelIndex?(0===n.fragmentError&&(n.loadError=0),this.playlistLoaded(i,e,n.details)):null!=(s=e.deliveryDirectives)&&s.skip&&(r.deltaUpdateFailed=!0)}loadPlaylist(t){super.loadPlaylist();const e=this.currentLevelIndex,s=this.currentLevel;if(s&&this.shouldLoadPlaylist(s)){let i=s.uri;if(t)try{i=t.addDirectives(i)}catch(t){this.warn(`Could not construct new URL with HLS Delivery Directives: ${t}`)}const r=s.attrs["PATHWAY-ID"];this.log(`Loading level index ${e}${void 0!==(null==t?void 0:t.msn)?" at sn "+t.msn+" part "+t.part:""} with${r?" Pathway "+r:""} ${i}`),this.clearTimer(),this.hls.trigger(p.LEVEL_LOADING,{url:i,level:e,pathwayId:s.attrs["PATHWAY-ID"],id:0,deliveryDirectives:t||null})}}get nextLoadLevel(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel}set nextLoadLevel(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}removeLevel(t){var e;const s=this._levels.filter(((e,s)=>s!==t||(this.steering&&this.steering.removeLevel(e),e===this.currentLevel&&(this.currentLevel=null,this.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((t=>t.level=-1))),!1)));ds(s),this._levels=s,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(p.LEVELS_UPDATED,{levels:s})}onLevelsUpdated(t,{levels:e}){this._levels=e}checkMaxAutoUpdated(){const{autoLevelCapping:t,maxAutoLevel:e,maxHdcpLevel:s}=this.hls;this._maxAutoLevel!==e&&(this._maxAutoLevel=e,this.hls.trigger(p.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:t,levels:this.levels,maxAutoLevel:e,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:s}))}}function wa(t){const e={};t.forEach((t=>{const s=t.groupId||"";t.id=e[s]=e[s]||0,e[s]++}))}class Da{constructor(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}abort(t){for(const s in this.keyUriToKeyInfo){const i=this.keyUriToKeyInfo[s].loader;if(i){var e;if(t&&t!==(null==(e=i.context)?void 0:e.frag.type))return;i.abort()}}}detach(){for(const t in this.keyUriToKeyInfo){const e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}}destroy(){this.detach();for(const t in this.keyUriToKeyInfo){const e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}}createKeyLoadError(t,e=y.KEY_LOAD_ERROR,s,i,r){return new ai({type:v.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:r,error:s,networkDetails:i})}loadClear(t,e){if(this.emeController&&this.config.emeEnabled){const{sn:s,cc:i}=t;for(let t=0;t{r.setKeyFormat(t)}));break}}}}load(t){return!t.decryptdata&&t.encrypted&&this.emeController?this.emeController.selectKeySystemFormat(t).then((e=>this.loadInternal(t,e))):this.loadInternal(t)}loadInternal(t,e){var s,i;e&&t.setKeyFormat(e);const r=t.decryptdata;if(!r){const s=new Error(e?`Expected frag.decryptdata to be defined after setting format ${e}`:"Missing decryption data on fragment in onKeyLoading");return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,s))}const n=r.uri;if(!n)return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,new Error(`Invalid key URI: "${n}"`)));let a=this.keyUriToKeyInfo[n];if(null!=(s=a)&&s.decryptdata.key)return r.key=a.decryptdata.key,Promise.resolve({frag:t,keyInfo:a});var o;if(null!=(i=a)&&i.keyLoadPromise)switch(null==(o=a.mediaKeySessionContext)?void 0:o.keyStatus){case void 0:case"status-pending":case"usable":case"usable-in-future":return a.keyLoadPromise.then((e=>(r.key=e.keyInfo.decryptdata.key,{frag:t,keyInfo:a})))}switch(a=this.keyUriToKeyInfo[n]={decryptdata:r,keyLoadPromise:null,loader:null,mediaKeySessionContext:null},r.method){case"ISO-23001-7":case"SAMPLE-AES":case"SAMPLE-AES-CENC":case"SAMPLE-AES-CTR":return"identity"===r.keyFormat?this.loadKeyHTTP(a,t):this.loadKeyEME(a,t);case"AES-128":return this.loadKeyHTTP(a,t);default:return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,new Error(`Key supplied with unsupported METHOD: "${r.method}"`)))}}loadKeyEME(t,e){const s={frag:e,keyInfo:t};if(this.emeController&&this.config.emeEnabled){const e=this.emeController.loadKey(s);if(e)return(t.keyLoadPromise=e.then((e=>(t.mediaKeySessionContext=e,s)))).catch((e=>{throw t.keyLoadPromise=null,e}))}return Promise.resolve(s)}loadKeyHTTP(t,e){const s=this.config,i=new(0,s.loader)(s);return e.keyLoader=t.loader=i,t.keyLoadPromise=new Promise(((r,n)=>{const a={keyInfo:t,frag:e,responseType:"arraybuffer",url:t.decryptdata.uri},o=s.keyLoadPolicy.default,l={loadPolicy:o,timeout:o.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0},d={onSuccess:(t,e,s,i)=>{const{frag:a,keyInfo:o,url:l}=s;if(!a.decryptdata||o!==this.keyUriToKeyInfo[l])return n(this.createKeyLoadError(a,y.KEY_LOAD_ERROR,new Error("after key load, decryptdata unset or changed"),i));o.decryptdata.key=a.decryptdata.key=new Uint8Array(t.data),a.keyLoader=null,o.loader=null,r({frag:a,keyInfo:o})},onError:(t,s,i,r)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.KEY_LOAD_ERROR,new Error(`HTTP Error ${t.code} loading key ${t.text}`),i,h({url:a.url,data:void 0},t)))},onTimeout:(t,s,i)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.KEY_LOAD_TIMEOUT,new Error("key loading timed out"),i))},onAbort:(t,s,i)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.INTERNAL_ABORTED,new Error("key loading aborted"),i))}};i.load(a,l,d)}))}resetLoader(t){const{frag:e,keyInfo:s,url:i}=t,r=s.loader;e.keyLoader===r&&(e.keyLoader=null,s.loader=null),delete this.keyUriToKeyInfo[i],r&&r.destroy()}}function Ia(){return self.SourceBuffer||self.WebKitSourceBuffer}function _a(){if(!te())return!1;const t=Ia();return!t||t.prototype&&"function"==typeof t.prototype.appendBuffer&&"function"==typeof t.prototype.remove}class Ca{constructor(t,e,s,i){this.config=void 0,this.media=null,this.fragmentTracker=void 0,this.hls=void 0,this.nudgeRetry=0,this.stallReported=!1,this.stalled=null,this.moved=!1,this.seeking=!1,this.config=t,this.media=e,this.fragmentTracker=s,this.hls=i}destroy(){this.media=null,this.hls=this.fragmentTracker=null}poll(t,e){const{config:s,media:i,stalled:r}=this;if(null===i)return;const{currentTime:n,seeking:a}=i,o=this.seeking&&!a,l=!this.seeking&&a;if(this.seeking=a,n!==t){if(this.moved=!0,a||(this.nudgeRetry=0),null!==r){if(this.stallReported){const t=self.performance.now()-r;A.warn(`playback not stuck anymore @${n}, after ${Math.round(t)}ms`),this.stallReported=!1}this.stalled=null}return}if(l||o)return void(this.stalled=null);if(i.paused&&!a||i.ended||0===i.playbackRate||!Xs.getBuffered(i).length)return void(this.nudgeRetry=0);const h=Xs.bufferInfo(i,n,0),d=h.nextStart||0;if(a){const t=h.len>2,s=!d||e&&e.start<=n||d-n>2&&!this.fragmentTracker.getPartialFragment(n);if(t||s)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var c;if(!(h.len>0)&&!d)return;const t=Math.max(d,h.start||0)-n,e=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,s=(null==e||null==(c=e.details)?void 0:c.live)?2*e.details.targetduration:2,r=this.fragmentTracker.getPartialFragment(n);if(t>0&&(t<=s||r))return void(i.paused||this._trySkipBufferHole(r))}const u=self.performance.now();if(null===r)return void(this.stalled=u);const f=u-r;if(!a&&f>=250&&(this._reportStall(h),!this.media))return;const g=Xs.bufferInfo(i,n,s.maxBufferHole);this._tryFixBufferStall(g,f)}_tryFixBufferStall(t,e){const{config:s,fragmentTracker:i,media:r}=this;if(null===r)return;const n=r.currentTime,a=i.getPartialFragment(n);if(a){if(this._trySkipBufferHole(a)||!this.media)return}(t.len>s.maxBufferHole||t.nextStart&&t.nextStart-n1e3*s.highBufferWatchdogPeriod&&(A.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}_reportStall(t){const{hls:e,media:s,stallReported:i}=this;if(!i&&s){this.stallReported=!0;const i=new Error(`Playback stalling at @${s.currentTime} due to low buffer (${JSON.stringify(t)})`);A.warn(i.message),e.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}}_trySkipBufferHole(t){const{config:e,hls:s,media:i}=this;if(null===i)return 0;const r=i.currentTime,n=Xs.bufferInfo(i,r,0),a=r0&&n.len<1&&i.readyState<3,h=a-r;if(h>0&&(o||l)){if(h>e.maxBufferHole){const{fragmentTracker:e}=this;let s=!1;if(0===r){const t=e.getAppendedFrag(0,De);t&&a1?(t=0,this.bitrateTest=!0):t=s.firstAutoLevel),s.nextLoadLevel=t,this.level=s.loadLevel,this.loadedmetadata=!1}e>0&&-1===t&&(this.log(`Override startPosition with lastCurrentTime @${e.toFixed(3)}`),t=e),this.state=fi,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=ui}stopLoad(){this._forceStartLoad=!1,super.stopLoad()}doTick(){switch(this.state){case Ai:{const{levels:t,level:e}=this,s=null==t?void 0:t[e],i=null==s?void 0:s.details;if(i&&(!i.live||this.levelLastLoaded===s)){if(this.waitForCdnTuneIn(i))break;this.state=fi;break}if(this.hls.nextLoadLevel!==this.level){this.state=fi;break}break}case pi:{var t;const e=self.performance.now(),s=this.retryDate;if(!s||e>=s||null!=(t=this.media)&&t.seeking){const{levels:t,level:e}=this,s=null==t?void 0:t[e];this.resetStartWhenNotLoaded(s||null),this.state=fi}}}this.state===fi&&this.doTickIdle(),this.onTickEnd()}onTickEnd(){super.onTickEnd(),this.checkBuffer(),this.checkFragmentChanged()}doTickIdle(){const{hls:t,levelLastLoaded:e,levels:s,media:i}=this;if(null===e||!i&&(this.startFragRequested||!t.config.startFragPrefetch))return;if(this.altAudio&&this.audioOnly)return;const r=t.nextLoadLevel;if(null==s||!s[r])return;const n=s[r],a=this.getMainFwdBufferInfo();if(null===a)return;const o=this.getLevelDetails();if(o&&this._streamEnded(a,o)){const t={};return this.altAudio&&(t.type="video"),this.hls.trigger(p.BUFFER_EOS,t),void(this.state=Ti)}t.loadLevel!==r&&-1===t.manualLevel&&this.log(`Adapting to level ${r} from level ${this.level}`),this.level=t.nextLoadLevel=r;const l=n.details;if(!l||this.state===Ai||l.live&&this.levelLastLoaded!==n)return this.level=r,void(this.state=Ai);const h=a.len,d=this.getMaxBufferLength(n.maxBitrate);if(h>=d)return;this.backtrackFragment&&this.backtrackFragment.start>a.end&&(this.backtrackFragment=null);const c=this.backtrackFragment?this.backtrackFragment.start:a.end;let u=this.getNextFragment(c,l);if(this.couldBacktrack&&!this.fragPrevious&&u&&"initSegment"!==u.sn&&this.fragmentTracker.getState(u)!==Vs){var f;const t=(null!=(f=this.backtrackFragment)?f:u).sn-l.startSN,e=l.fragments[t-1];e&&u.cc===e.cc&&(u=e,this.fragmentTracker.removeFragment(e))}else this.backtrackFragment&&a.len&&(this.backtrackFragment=null);if(u&&this.isLoopLoading(u,c)){if(!u.gap){const t=this.audioOnly&&!this.altAudio?_:C,e=(t===C?this.videoBuffer:this.mediaBuffer)||this.media;e&&this.afterBufferFlushed(e,t,De)}u=this.getNextFragmentLoopLoading(u,l,a,De,d)}u&&(!u.initSegment||u.initSegment.data||this.bitrateTest||(u=u.initSegment),this.loadFragment(u,n,c))}loadFragment(t,e,s){const i=this.fragmentTracker.getState(t);this.fragCurrent=t,i===Gs||i===Hs?"initSegment"===t.sn?this._loadInitSegment(t,e):this.bitrateTest?(this.log(`Fragment ${t.sn} of level ${t.level} is being downloaded to test bitrate and will not be buffered`),this._loadBitrateTestFrag(t,e)):(this.startFragRequested=!0,super.loadFragment(t,e,s)):this.clearTrackerIfNeeded(t)}getBufferedFrag(t){return this.fragmentTracker.getBufferedFrag(t,De)}followingBufferedFrag(t){return t?this.getBufferedFrag(t.end+.5):null}immediateLevelSwitch(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)}nextLevelSwitch(){const{levels:t,media:e}=this;if(null!=e&&e.readyState){let s;const i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);const r=this.getLevelDetails();if(null!=r&&r.live){const t=this.getMainFwdBufferInfo();if(!t||t.len<2*r.targetduration)return}if(!e.paused&&t){const e=t[this.hls.nextLoadLevel],i=this.fragLastKbps;s=i&&this.fragCurrent?this.fragCurrent.duration*e.maxBitrate/(1e3*i)+1:0}else s=0;const n=this.getBufferedFrag(e.currentTime+s);if(n){const t=this.followingBufferedFrag(n);if(t){this.abortCurrentFrag();const e=t.maxStartPTS?t.maxStartPTS:t.start,s=t.duration,i=Math.max(n.end,e+Math.min(Math.max(s-this.config.maxFragLookUpTolerance,s*(this.couldBacktrack?.5:.125)),s*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(i,Number.POSITIVE_INFINITY)}}}}abortCurrentFrag(){const t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case gi:case mi:case pi:case yi:case Ei:this.state=fi}this.nextLoadPosition=this.getLoadPosition()}flushMainBuffer(t,e){super.flushMainBuffer(t,e,this.altAudio?"video":null)}onMediaAttached(t,e){super.onMediaAttached(t,e);const s=e.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),s.addEventListener("playing",this.onvplaying),s.addEventListener("seeked",this.onvseeked),this.gapController=new Ca(this.config,s,this.fragmentTracker,this.hls)}onMediaDetaching(){const{media:t}=this;t&&this.onvplaying&&this.onvseeked&&(t.removeEventListener("playing",this.onvplaying),t.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),super.onMediaDetaching()}onMediaPlaying(){this.tick()}onMediaSeeked(){const t=this.media,e=t?t.currentTime:null;f(e)&&this.log(`Media seeked to ${e.toFixed(3)}`);const s=this.getMainFwdBufferInfo();null!==s&&0!==s.len?this.tick():this.warn(`Main forward buffer length on "seeked" event ${s?s.len:"empty"})`)}onManifestLoading(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(p.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1}onManifestParsed(t,e){let s=!1,i=!1;e.levels.forEach((t=>{const e=t.audioCodec;e&&(s=s||-1!==e.indexOf("mp4a.40.2"),i=i||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=s&&i&&!function(){var t;const e=Ia();return"function"==typeof(null==e||null==(t=e.prototype)?void 0:t.changeType)}(),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1}onLevelLoading(t,e){const{levels:s}=this;if(!s||this.state!==fi)return;const i=s[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=Ai)}onLevelLoaded(t,e){var s;const{levels:i}=this,r=e.level,n=e.details,a=n.totalduration;if(!i)return void this.warn(`Levels were reset while loading level ${r}`);this.log(`Level ${r} loaded [${n.startSN},${n.endSN}]${n.lastPartSn?`[part-${n.lastPartSn}-${n.lastPartIndex}]`:""}, cc [${n.startCC}, ${n.endCC}] duration:${a}`);const o=i[r],l=this.fragCurrent;!l||this.state!==mi&&this.state!==pi||l.level!==e.level&&l.loader&&this.abortCurrentFrag();let h=0;if(n.live||null!=(s=o.details)&&s.live){var d;if(this.checkLiveUpdate(n),n.deltaUpdateFailed)return;h=this.alignPlaylists(n,o.details,null==(d=this.levelLastLoaded)?void 0:d.details)}if(o.details=n,this.levelLastLoaded=o,this.hls.trigger(p.LEVEL_UPDATED,{details:n,level:r}),this.state===Ai){if(this.waitForCdnTuneIn(n))return;this.state=fi}this.startFragRequested?n.live&&this.synchronizeToLiveEdge(n):this.setStartPosition(n,h),this.tick()}_handleFragmentLoadProgress(t){var e;const{frag:s,part:i,payload:r}=t,{levels:n}=this;if(!n)return void this.warn(`Levels were reset while fragment load was in progress. Fragment ${s.sn} of level ${s.level} will not be buffered`);const a=n[s.level],o=a.details;if(!o)return this.warn(`Dropping fragment ${s.sn} of level ${s.level} after level details were reset`),void this.fragmentTracker.removeFragment(s);const l=a.videoCodec,h=o.PTSKnown||!o.live,d=null==(e=s.initSegment)?void 0:e.data,c=this._getAudioCodec(a),u=this.transmuxer=this.transmuxer||new Cr(this.hls,De,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,m=new zs(s.level,s.sn,s.stats.chunkCount,r.byteLength,f,g),p=this.initPTS[s.cc];u.push(r,d,c,l,s,i,o.totalduration,h,m,p)}onAudioTrackSwitching(t,e){const s=this.altAudio;if(!!!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;const t=this.fragCurrent;t&&(this.log("Switching to main audio track, cancel main fragment load"),t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();const t=this.hls;s&&(t.trigger(p.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),t.trigger(p.AUDIO_TRACK_SWITCHED,e)}}onAudioTrackSwitched(t,e){const s=e.id,i=!!this.hls.audioTracks[s].url;if(i){const t=this.videoBuffer;t&&this.mediaBuffer!==t&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=t)}this.altAudio=i,this.tick()}onBufferCreated(t,e){const s=e.tracks;let i,r,n=!1;for(const t in s){const e=s[t];if("main"===e.id){if(r=t,i=e,"video"===t){const e=s[t];e&&(this.videoBuffer=e.buffer)}}else n=!0}n&&i?(this.log(`Alternate track found, use ${r}.buffered to schedule main fragment loading`),this.mediaBuffer=i.buffer):this.mediaBuffer=this.media}onFragBuffered(t,e){const{frag:s,part:i}=e;if(s&&s.type!==De)return;if(this.fragContextChanged(s))return this.warn(`Fragment ${s.sn}${i?" p: "+i.index:""} of level ${s.level} finished buffering, but was aborted. state: ${this.state}`),void(this.state===Ei&&(this.state=fi));const r=i?i.stats:s.stats;this.fragLastKbps=Math.round(8*r.total/(r.buffering.end-r.loading.first)),"initSegment"!==s.sn&&(this.fragPrevious=s),this.fragBufferedComplete(s,i)}onError(t,e){var s;if(e.fatal)this.state=Si;else switch(e.details){case y.FRAG_GAP:case y.FRAG_PARSING_ERROR:case y.FRAG_DECRYPT_ERROR:case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(De,e);break;case y.LEVEL_LOAD_ERROR:case y.LEVEL_LOAD_TIMEOUT:case y.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==Ai||(null==(s=e.context)?void 0:s.type)!==be||(this.state=fi);break;case y.BUFFER_APPEND_ERROR:case y.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===y.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case y.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}}checkBuffer(){const{media:t,gapController:e}=this;if(t&&e&&t.readyState){if(this.loadedmetadata||!Xs.getBuffered(t).length){const t=this.state!==fi?this.fragCurrent:null;e.poll(this.lastCurrentTime,t)}this.lastCurrentTime=t.currentTime}}onFragLoadEmergencyAborted(){this.state=fi,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()}onBufferFlushed(t,{type:e}){if(e!==_||this.audioOnly&&!this.altAudio){const t=(e===C?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(t,e,De),this.tick()}}onLevelsUpdated(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels}swapAudioCodec(){this.audioCodecSwap=!this.audioCodecSwap}seekToStartPos(){const{media:t}=this;if(!t)return;const e=t.currentTime;let s=this.startPosition;if(s>=0&&e0&&(r{const{hls:i}=this;if(!s||this.fragContextChanged(t))return;e.fragmentError=0,this.state=fi,this.startFragRequested=!1,this.bitrateTest=!1;const r=t.stats;r.parsing.start=r.parsing.end=r.buffering.start=r.buffering.end=self.performance.now(),i.trigger(p.FRAG_LOADED,s),t.bitrateTest=!1}))}_handleTransmuxComplete(t){var e;const s="main",{hls:i}=this,{remuxResult:r,chunkMeta:n}=t,a=this.getCurrentContext(n);if(!a)return void this.resetWhenMissingContext(n);const{frag:o,part:l,level:h}=a,{video:d,text:c,id3:u,initSegment:g}=r,{details:m}=h,v=this.altAudio?void 0:r.audio;if(this.fragContextChanged(o))this.fragmentTracker.removeFragment(o);else{if(this.state=yi,g){if(null!=g&&g.tracks){const t=o.initSegment||o;this._bufferInitSegment(h,g.tracks,t,n),i.trigger(p.FRAG_PARSING_INIT_SEGMENT,{frag:t,id:s,tracks:g.tracks})}const t=g.initPTS,e=g.timescale;f(t)&&(this.initPTS[o.cc]={baseTime:t,timescale:e},i.trigger(p.INIT_PTS_FOUND,{frag:o,id:s,initPTS:t,timescale:e}))}if(d&&m&&"initSegment"!==o.sn){const t=m.fragments[o.sn-1-m.startSN],e=o.sn===m.startSN,s=!t||o.cc>t.cc;if(!1!==r.independent){const{startPTS:t,endPTS:i,startDTS:r,endDTS:a}=d;if(l)l.elementaryStreams[d.type]={startPTS:t,endPTS:i,startDTS:r,endDTS:a};else if(d.firstKeyFrame&&d.independent&&1===n.id&&!s&&(this.couldBacktrack=!0),d.dropped&&d.independent){const r=this.getMainFwdBufferInfo(),n=(r?r.end:this.getLoadPosition())+this.config.maxBufferHole,l=d.firstKeyFramePTS?d.firstKeyFramePTS:t;if(!e&&n2&&(o.gap=!0);o.setElementaryStreamInfo(d.type,t,i,r,a),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(d,o,l,n,e||s)}else{if(!e&&!s)return void this.backtrack(o);o.gap=!0}}if(v){const{startPTS:t,endPTS:e,startDTS:s,endDTS:i}=v;l&&(l.elementaryStreams[_]={startPTS:t,endPTS:e,startDTS:s,endDTS:i}),o.setElementaryStreamInfo(_,t,e,s,i),this.bufferFragmentData(v,o,l,n)}if(m&&null!=u&&null!=(e=u.samples)&&e.length){const t={id:s,frag:o,details:m,samples:u.samples};i.trigger(p.FRAG_PARSING_METADATA,t)}if(m&&c){const t={id:s,frag:o,details:m,samples:c.samples};i.trigger(p.FRAG_PARSING_USERDATA,t)}}}_bufferInitSegment(t,e,s,i){if(this.state!==yi)return;this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;const{audio:r,video:n,audiovideo:a}=e;if(r){let e=t.audioCodec;const s=navigator.userAgent.toLowerCase();if(this.audioCodecSwitch){e&&(e=-1!==e.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5");const t=r.metadata;t&&"channelCount"in t&&1!==(t.channelCount||1)&&-1===s.indexOf("firefox")&&(e="mp4a.40.5")}e&&-1!==e.indexOf("mp4a.40.5")&&-1!==s.indexOf("android")&&"audio/mpeg"!==r.container&&(e="mp4a.40.2",this.log(`Android: force audio codec to ${e}`)),t.audioCodec&&t.audioCodec!==e&&this.log(`Swapping manifest audio codec "${t.audioCodec}" for "${e}"`),r.levelCodec=e,r.id="main",this.log(`Init audio buffer, container:${r.container}, codecs[selected/level/parsed]=[${e||""}/${t.audioCodec||""}/${r.codec}]`)}n&&(n.levelCodec=t.videoCodec,n.id="main",this.log(`Init video buffer, container:${n.container}, codecs[level/parsed]=[${t.videoCodec||""}/${n.codec}]`)),a&&this.log(`Init audiovideo buffer, container:${a.container}, codecs[level/parsed]=[${t.codecs}/${a.codec}]`),this.hls.trigger(p.BUFFER_CODECS,e),Object.keys(e).forEach((t=>{const r=e[t].initSegment;null!=r&&r.byteLength&&this.hls.trigger(p.BUFFER_APPENDING,{type:t,data:r,frag:s,part:null,chunkMeta:i,parent:s.type})})),this.tickImmediate()}getMainFwdBufferInfo(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,De)}backtrack(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=fi}checkFragmentChanged(){const t=this.media;let e=null;if(t&&t.readyState>1&&!1===t.seeking){const s=t.currentTime;if(Xs.isBuffered(t,s)?e=this.getAppendedFrag(s):Xs.isBuffered(t,s+.1)&&(e=this.getAppendedFrag(s+.1)),e){this.backtrackFragment=null;const t=this.fragPlaying,s=e.level;t&&e.sn===t.sn&&t.level===s||(this.fragPlaying=e,this.hls.trigger(p.FRAG_CHANGED,{frag:e}),t&&t.level===s||this.hls.trigger(p.LEVEL_SWITCHED,{level:s}))}}}get nextLevel(){const t=this.nextBufferedFrag;return t?t.level:-1}get currentFrag(){const t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}get currentProgramDateTime(){const t=this.media;if(t){const e=t.currentTime,s=this.currentFrag;if(s&&f(e)&&f(s.programDateTime)){const t=s.programDateTime+1e3*(e-s.start);return new Date(t)}}return null}get currentLevel(){const t=this.currentFrag;return t?t.level:-1}get nextBufferedFrag(){const t=this.currentFrag;return t?this.followingBufferedFrag(t):null}get forceStartLoad(){return this._forceStartLoad}}class Pa{static get version(){return"1.5.15"}static isMSESupported(){return _a()}static isSupported(){return function(){if(!_a())return!1;const t=te();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((e=>t.isTypeSupported(re(e,"video"))))||["mp4a.40.2","fLaC"].some((e=>t.isTypeSupported(re(e,"audio")))))}()}static getMediaSource(){return te()}static get Events(){return p}static get ErrorTypes(){return v}static get ErrorDetails(){return y}static get DefaultConfig(){return Pa.defaultConfig?Pa.defaultConfig:La}static set DefaultConfig(t){Pa.defaultConfig=t}constructor(t={}){this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new _r,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,function(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){L(t,"debug","log","info","warn","error");try{S.log(`Debug logs enabled for "${e}" in hls.js version 1.5.15`)}catch(t){S=T}}else S=T}(t.debug||!1,"Hls instance");const e=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');const s=Aa(t),i=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((t=>{const r=`${"level"===t?"playlist":t}LoadPolicy`,n=void 0===e[r],a=[];i.forEach((i=>{const o=`${t}Loading${i}`,l=e[o];if(void 0!==l&&n){a.push(o);const t=s[r].default;switch(e[r]={default:t},i){case"TimeOut":t.maxLoadTimeMs=l,t.maxTimeToFirstByteMs=l;break;case"MaxRetry":t.errorRetry.maxNumRetry=l,t.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":t.errorRetry.retryDelayMs=l,t.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":t.errorRetry.maxRetryDelayMs=l,t.timeoutRetry.maxRetryDelayMs=l}}})),a.length&&A.warn(`hls.js config: "${a.join('", "')}" setting(s) are deprecated, use "${r}": ${JSON.stringify(e[r])}`)})),h(h({},s),e)}(Pa.DefaultConfig,t);this.userConfig=t,e.progressive&&Ra(e);const{abrController:s,bufferController:i,capLevelController:r,errorController:n,fpsController:a}=e,o=new n(this),l=this.abrController=new s(this),d=this.bufferController=new i(this),c=this.capLevelController=new r(this),u=new a(this),f=new Pe(this),g=new We(this),m=e.contentSteeringController,v=m?new m(this):null,y=this.levelController=new ka(this,v),E=new Ys(this),R=new Da(this.config),b=this.streamController=new xa(this,E,R);c.setStreamController(b),u.setStreamController(b);const k=[f,y,b];v&&k.splice(1,0,v),this.networkControllers=k;const w=[l,d,c,u,g,E];this.audioTrackController=this.createController(e.audioTrackController,k);const D=e.audioStreamController;D&&k.push(new D(this,E,R)),this.subtitleTrackController=this.createController(e.subtitleTrackController,k);const I=e.subtitleStreamController;I&&k.push(new I(this,E,R)),this.createController(e.timelineController,w),R.emeController=this.emeController=this.createController(e.emeController,w),this.cmcdController=this.createController(e.cmcdController,w),this.latencyController=this.createController(je,w),this.coreComponents=w,k.push(o);const _=o.onErrorOut;"function"==typeof _&&this.on(p.ERROR,_,o)}createController(t,e){if(t){const s=new t(this);return e&&e.push(s),s}return null}on(t,e,s=this){this._emitter.on(t,e,s)}once(t,e,s=this){this._emitter.once(t,e,s)}removeAllListeners(t){this._emitter.removeAllListeners(t)}off(t,e,s=this,i){this._emitter.off(t,e,s,i)}listeners(t){return this._emitter.listeners(t)}emit(t,e,s){return this._emitter.emit(t,e,s)}trigger(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(A.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;const s=t===p.ERROR;this.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,fatal:s,event:t,error:e}),this.triggeringException=!1}}return!1}listenerCount(t){return this._emitter.listenerCount(t)}destroy(){A.log("destroy"),this.trigger(p.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((t=>t.destroy())),this.networkControllers.length=0,this.coreComponents.forEach((t=>t.destroy())),this.coreComponents.length=0;const t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null}attachMedia(t){A.log("attachMedia"),this._media=t,this.trigger(p.MEDIA_ATTACHING,{media:t})}detachMedia(){A.log("detachMedia"),this.trigger(p.MEDIA_DETACHING,void 0),this._media=null}loadSource(t){this.stopLoad();const e=this.media,s=this.url,i=this.url=o.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,A.log(`loadSource:${i}`),e&&s&&(s!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(p.MANIFEST_LOADING,{url:t})}startLoad(t=-1){A.log(`startLoad(${t})`),this.started=!0,this.networkControllers.forEach((e=>{e.startLoad(t)}))}stopLoad(){A.log("stopLoad"),this.started=!1,this.networkControllers.forEach((t=>{t.stopLoad()}))}resumeBuffering(){this.started&&this.networkControllers.forEach((t=>{"fragmentLoader"in t&&t.startLoad(-1)}))}pauseBuffering(){this.networkControllers.forEach((t=>{"fragmentLoader"in t&&t.stopLoad()}))}swapAudioCodec(){A.log("swapAudioCodec"),this.streamController.swapAudioCodec()}recoverMediaError(){A.log("recoverMediaError");const t=this._media;this.detachMedia(),t&&this.attachMedia(t)}removeLevel(t){this.levelController.removeLevel(t)}get levels(){const t=this.levelController.levels;return t||[]}get currentLevel(){return this.streamController.currentLevel}set currentLevel(t){A.log(`set currentLevel:${t}`),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}get nextLevel(){return this.streamController.nextLevel}set nextLevel(t){A.log(`set nextLevel:${t}`),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}get loadLevel(){return this.levelController.level}set loadLevel(t){A.log(`set loadLevel:${t}`),this.levelController.manualLevel=t}get nextLoadLevel(){return this.levelController.nextLoadLevel}set nextLoadLevel(t){this.levelController.nextLoadLevel=t}get firstLevel(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)}set firstLevel(t){A.log(`set firstLevel:${t}`),this.levelController.firstLevel=t}get startLevel(){const t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t}set startLevel(t){A.log(`set startLevel:${t}`),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}get capLevelToPlayerSize(){return this.config.capLevelToPlayerSize}set capLevelToPlayerSize(t){const e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}get autoLevelCapping(){return this._autoLevelCapping}get bandwidthEstimate(){const{bwEstimator:t}=this.abrController;return t?t.getEstimate():NaN}set bandwidthEstimate(t){this.abrController.resetEstimator(t)}get ttfbEstimate(){const{bwEstimator:t}=this.abrController;return t?t.getEstimateTTFB():NaN}set autoLevelCapping(t){this._autoLevelCapping!==t&&(A.log(`set autoLevelCapping:${t}`),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}get maxHdcpLevel(){return this._maxHdcpLevel}set maxHdcpLevel(t){(function(t){return qe.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,this.levelController.checkMaxAutoUpdated())}get autoLevelEnabled(){return-1===this.levelController.manualLevel}get manualLevel(){return this.levelController.manualLevel}get minAutoLevel(){const{levels:t,config:{minAutoBitrate:e}}=this;if(!t)return 0;const s=t.length;for(let i=0;i=e)return i;return 0}get maxAutoLevel(){const{levels:t,autoLevelCapping:e,maxHdcpLevel:s}=this;let i;if(i=-1===e&&null!=t&&t.length?t.length-1:e,s)for(let e=i;e--;){const i=t[e].attrs["HDCP-LEVEL"];if(i&&i<=s)return e}return i}get firstAutoLevel(){return this.abrController.firstAutoLevel}get nextAutoLevel(){return this.abrController.nextAutoLevel}set nextAutoLevel(t){this.abrController.nextAutoLevel=t}get playingDate(){return this.streamController.currentProgramDateTime}get mainForwardBufferInfo(){return this.streamController.getMainFwdBufferInfo()}setAudioOption(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)}setSubtitleOption(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null}get allAudioTracks(){const t=this.audioTrackController;return t?t.allAudioTracks:[]}get audioTracks(){const t=this.audioTrackController;return t?t.audioTracks:[]}get audioTrack(){const t=this.audioTrackController;return t?t.audioTrack:-1}set audioTrack(t){const e=this.audioTrackController;e&&(e.audioTrack=t)}get allSubtitleTracks(){const t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}get subtitleTracks(){const t=this.subtitleTrackController;return t?t.subtitleTracks:[]}get subtitleTrack(){const t=this.subtitleTrackController;return t?t.subtitleTrack:-1}get media(){return this._media}set subtitleTrack(t){const e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}get subtitleDisplay(){const t=this.subtitleTrackController;return!!t&&t.subtitleDisplay}set subtitleDisplay(t){const e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}get lowLatencyMode(){return this.config.lowLatencyMode}set lowLatencyMode(t){this.config.lowLatencyMode=t}get liveSyncPosition(){return this.latencyController.liveSyncPosition}get latency(){return this.latencyController.latency}get maxLatency(){return this.latencyController.maxLatency}get targetLatency(){return this.latencyController.targetLatency}get drift(){return this.latencyController.drift}get forceStartLoad(){return this.streamController.forceStartLoad}}Pa.defaultConfig=void 0;class Ma{constructor(){this._ranges=[]}get length(){return this._ranges.length}start(t){if(t<0||t>=this._ranges.length)throw new DOMException("Invalid index","IndexSizeError");return this._ranges[t].start}end(t){if(t<0||t>=this._ranges.length)throw new DOMException("Invalid index","IndexSizeError");return this._ranges[t].end}_addRange(t,e){this._ranges.push({start:t,end:e}),this._normalizeRanges()}_removeRange(t,e){let s=[];for(let i of this._ranges)i.end<=t||i.start>=e?s.push(i):i.starte?(s.push({start:i.start,end:t}),s.push({start:e,end:i.end})):i.start>=t&&i.end<=e||(i.startt&&i.end<=e?s.push({start:i.start,end:t}):i.start>=t&&i.starte&&s.push({start:e,end:i.end}));this._ranges=s}_normalizeRanges(){this._ranges.sort(((t,e)=>t.start-e.start));let t=[];for(let e of this._ranges)if(0===t.length)t.push(e);else{let s=t[t.length-1];e.start<=s.end?s.end=Math.max(s.end,e.end):t.push(e)}this._ranges=t}}class Fa extends EventTarget{constructor(t="",e="",s=""){super(),this.kind=t,this.label=e,this.language=s,this.mode="disabled",this.cues=new Oa,this.activeCues=new Oa}addCue(t){this.cues._add(t)}removeCue(t){this.cues._remove(t)}}class Oa{constructor(){this._cues=[]}get length(){return this._cues.length}item(t){return this._cues[t]}getCueById(t){return this._cues.find((e=>e.id===t))||null}_add(t){this._cues.push(t)}_remove(t){const e=this._cues.indexOf(t);-1!==e&&this._cues.splice(e,1)}[Symbol.iterator](){return this._cues[Symbol.iterator]()}}class Na extends EventTarget{constructor(){super(),this._tracks=[]}get length(){return this._tracks.length}item(t){return this._tracks[t]}_add(t){this._tracks.push(t),this.dispatchEvent(new Event("addtrack"))}_remove(t){const e=this._tracks.indexOf(t);-1!==e&&(this._tracks.splice(e,1),this.dispatchEvent(new Event("removetrack")))}[Symbol.iterator](){return this._tracks[Symbol.iterator]()}}class Ua extends EventTarget{constructor(t){super(),this.instanceId=t,this.bridgeId=window.nextInternalId,window.nextInternalId+=1,window.bridgeObjectMap[this.bridgeId]=this,this._currentTime=0,this.duration=NaN,this.paused=!0,this._playbackRate=1,this.volume=1,this.muted=!1,this.readyState=0,this.networkState=0,this.buffered=new Ma,this.seeking=!1,this.loop=!1,this.autoplay=!1,this.controls=!1,this.error=null,this._src="",this.videoWidth=0,this.videoHeight=0,this.textTracks=new Na,this.isWaiting=!1,this.currentMedia=null,window.bridgeInvokeAsync(this.bridgeId,"VideoElement","constructor",{instanceId:this.instanceId}),setTimeout((()=>{this.readyState=4,this.dispatchEvent(new Event("loadedmetadata")),this.dispatchEvent(new Event("loadeddata")),this.dispatchEvent(new Event("canplay")),this.dispatchEvent(new Event("canplaythrough"))}),0)}get currentTime(){return this._currentTime}set currentTime(t){this._currentTime!=t&&(this._currentTime=t,this.dispatchEvent(new Event("seeking")),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setCurrentTime",{instanceId:this.instanceId,currentTime:t}).then((t=>{this.dispatchEvent(new Event("seeked"))})))}get playbackRate(){return this._playbackRate}set playbackRate(t){this._playbackRate=t,window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setPlaybackRate",{instanceId:this.instanceId,playbackRate:t}).then((t=>{}))}get src(){return this._src}set src(t){this.currentMedia&&this.currentMedia.removeEventListener("bufferChanged",!1),this._src=t;var e=window.mediaSourceMap[this._src];this.currentMedia=e,e&&(e.addEventListener("bufferChanged",(()=>{this.updateBufferedFromMediaSource()}),!1),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setMediaSource",{instanceId:this.instanceId,mediaSourceId:e.bridgeId}).then((t=>{})))}removeAttribute(t){"src"===t&&(this._src="")}querySelectorAll(t){return document.createDocumentFragment().querySelectorAll("*")}updateBufferedFromMediaSource(){var t=this.currentMedia;this.buffered._ranges=t?t.getBufferedRanges():[]}bridgeUpdateStatus(t){var e=!t.isPlaying,s=t.isWaiting,i=t.currentTime;this.paused!=e&&(this.paused=e,e?this.dispatchEvent(new Event("pause")):(this.dispatchEvent(new Event("play")),this.dispatchEvent(new Event("playing")))),this.isWaiting!=s&&(this.isWaiting=s,s&&this.dispatchEvent(new Event("waiting"))),this._currentTime!=i&&(this._currentTime=i,this.dispatchEvent(new Event("timeupdate")))}play(){return this.paused?window.bridgeInvokeAsync(this.bridgeId,"VideoElement","play",{instanceId:this.instanceId}).then((t=>{this.dispatchEvent(new Event("play")),this.dispatchEvent(new Event("playing"))})):Promise.resolve()}pause(){if(!this.paused)return this.paused=!0,this.dispatchEvent(new Event("pause")),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","pause",{instanceId:this.instanceId}).then((t=>{}))}canPlayType(t){return"probably"}addTextTrack(t,e,s){const i=new Fa(t,e,s);return this.textTracks._add(i),i}load(){}notifySeeked(){this.dispatchEvent(new Event("seeking")),this.dispatchEvent(new Event("seeked"))}}function Ba(t){const e=Array.from(t,(t=>String.fromCodePoint(t))).join("");return btoa(e)}class $a extends EventTarget{constructor(){super(),this._buffers=[]}_add(t){this._buffers.push(t),this.dispatchEvent(new Event("addsourcebuffer"))}_remove(t){const e=this._buffers.indexOf(t);return-1!==e&&(this._buffers.splice(e,1),this.dispatchEvent(new Event("removesourcebuffer")),!0)}get length(){return this._buffers.length}item(t){return this._buffers[t]}[Symbol.iterator](){return this._buffers[Symbol.iterator]()}}class Ga extends EventTarget{constructor(t,e){super(),this.mediaSource=t,this.mimeType=e,this.updating=!1,this.buffered=new Ma,this.timestampOffset=0,this.appendWindowStart=0,this.appendWindowEnd=1/0,this.bridgeId=window.nextInternalId,window.nextInternalId+=1,window.bridgeObjectMap[this.bridgeId]=this,window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","constructor",{mediaSourceId:this.mediaSource.bridgeId,mimeType:e})}appendBuffer(t){if(this.updating)throw new DOMException("SourceBuffer is updating","InvalidStateError");this.updating=!0,this.dispatchEvent(new Event("updatestart")),window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","appendBuffer",{data:Ba(t)}).then((t=>{const e=t.ranges;for(var s=[],i=0;i{})))}remove(t,e){if(this.updating)throw new DOMException("SourceBuffer is updating","InvalidStateError");this.updating=!0,this.dispatchEvent(new Event("updatestart")),window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","remove",{start:t,end:e}).then((t=>{const e=t.ranges;for(var s=[],i=0;i{this.readyState="open",this.dispatchEvent(new Event("sourceopen"))}),0)}static isTypeSupported(t){return!0}emitUpdatedBuffer(){this.dispatchEvent(new Event("bufferChanged"))}getBufferedRanges(){return 0!=this.sourceBuffers._buffers.length?this.sourceBuffers._buffers[0].buffered._ranges:[]}addSourceBuffer(t){if("open"!==this.readyState)throw new DOMException("MediaSource is not open","InvalidStateError");const e=new Ga(this,t);return this.sourceBuffers._add(e),this.activeSourceBuffers._add(e),this.dispatchEvent(new Event("bufferChanged")),window.bridgeInvokeAsync(this.bridgeId,"MediaSource","updateSourceBuffers",{ids:this.sourceBuffers._buffers.map((t=>t.bridgeId))}).then((t=>{})),e}removeSourceBuffer(t){if(!this.sourceBuffers._remove(t))throw new DOMException("SourceBuffer not found","NotFoundError");this.activeSourceBuffers._remove(t),this.dispatchEvent(new Event("bufferChanged")),window.bridgeInvokeAsync(this.bridgeId,"MediaSource","updateSourceBuffers",{ids:this.sourceBuffers._buffers.map((t=>t.bridgeId))}).then((t=>{}))}endOfStream(t){if("open"!==this.readyState)throw new DOMException("MediaSource is not open","InvalidStateError");this.readyState="ended",this.dispatchEvent(new Event("sourceended"))}_reopen(){"open"!==this.readyState&&(this.readyState="open",this.dispatchEvent(new Event("sourceopen")))}set duration(t){if("closed"===this.readyState)throw new DOMException("MediaSource is closed","InvalidStateError");this._duration=t,window.bridgeInvokeAsync(this.bridgeId,"MediaSource","setDuration",{duration:t}).then((t=>{}))}get duration(){return this._duration}}class Ha extends EventTarget{constructor(){super(),this.bridgeId=window.nextInternalId,window.nextInternalId+=1,this.readyState=0,this.status=0,this.statusText="",this.responseText="",this.responseXML=null,this._responseData=null,this.onreadystatechange=null,this._requestHeaders={},this._responseHeaders={},this._method="",this._url="",this._async=!0,this._user=null,this._password=null,this._responseType=""}open(t,e,s=!0,i=null,r=null){this._method=t,this._url=e,this._async=s,this._user=i,this._password=r,this.readyState=1,this._triggerReadyStateChange()}setRequestHeader(t,e){this._requestHeaders[t]=e}getResponseHeader(t){return this._responseHeaders[t.toLowerCase()]||null}getAllResponseHeaders(){return Object.entries(this._responseHeaders).map((([t,e])=>`${t}: ${e}`)).join("\r\n")}send(t=null){this.readyState=2,this._triggerReadyStateChange(),this.readyState=3,this._triggerReadyStateChange(),this.dispatchEvent(new Event("loadstart")),window.bridgeInvokeAsync(this.bridgeId,"XMLHttpRequest","load",{id:this.bridgeId,url:this._url,requestHeaders:this._requestHeaders}).then((t=>{t.error?this.dispatchEvent(new Event("error")):(this.status=t.status,this.statusText=t.statusText,t.responseData?("arraybuffer"===this._responseType?this._responseData=function(t){for(var e=atob(t),s=new Uint8Array(e.length),i=0;i{this.refreshPlayerStatus()})),this.video.addEventListener("pause",(()=>{this.refreshPlayerStatus()})),this.video.addEventListener("seeking",(()=>{this.refreshPlayerStatus()})),this.video.addEventListener("waiting",(()=>{this.refreshPlayerStatus()})),this.hls=new Pa({startLevel:0,testBandwidth:!1,debug:t.debug||!0,autoStartLoad:!1,backBufferLength:30,maxBufferLength:60,maxMaxBufferLength:60,maxFragLookUpTolerance:.001,nudgeMaxRetry:1e4}),this.hls.on(Pa.Events.MANIFEST_PARSED,(()=>{this.isManifestParsed=!0,this.refreshPlayerStatus()})),this.hls.on(Pa.Events.LEVEL_SWITCHED,(()=>{this.refreshPlayerStatus()})),this.hls.on(Pa.Events.LEVELS_UPDATED,(()=>{this.refreshPlayerStatus()})),this.hls.loadSource(t.urlPrefix+"master.m3u8"),this.hls.attachMedia(this.video)}playerLoad(t){this.hls.startLevel=t,this.hls.startLoad(-1,!1)}playerPlay(){this.video.play()}playerPause(){this.video.pause()}playerSetBaseRate(t){this.video.playbackRate=t}playerSetLevel(t){this.hls.currentLevel=t>=0?t:-1}playerSeek(t){this.video.currentTime=t}playerSetIsMuted(t){this.video.muted=t}getLevels(){for(var t=[],e=0;e2&&(t=!0),Va(this.id,"playerStatus",{isReady:this.isManifestParsed,isPlaying:!this.video.paused,rate:t?this.video.playbackRate:0,defaultRate:this.video.playbackRate,levels:this.getLevels(),currentLevel:this.hls.currentLevel}),this.refreshPlayerCurrentTime(),t?null==this.currentTimeUpdateTimeout&&(this.currentTimeUpdateTimeout=setTimeout((()=>{this.refreshPlayerCurrentTime()}),200)):null!=this.currentTimeUpdateTimeout&&(clearTimeout(this.currentTimeUpdateTimeout),this.currentTimeUpdateTimeout=null),this.notifySeekedOnNextStatusUpdate&&(this.notifySeekedOnNextStatusUpdate=!1,this.video.notifySeeked())}playerNotifySeekedOnNextStatusUpdate(){this.notifySeekedOnNextStatusUpdate=!0}refreshPlayerCurrentTime(){Va(this.id,"playerCurrentTime",{value:this.video.currentTime}),this.currentTimeUpdateTimeout=setTimeout((()=>{this.refreshPlayerCurrentTime()}),200)}}window.invokeOnLoad=function(){Va(this.id,"windowOnLoad",{})},window.onload=()=>{window.invokeOnLoad()},window.hlsPlayer_instances={},window.hlsPlayer_makeInstance=function(t){window.hlsPlayer_instances[t]=new Ya(t)},window.hlsPlayer_destroyInstance=function(t){const e=window.hlsPlayer_instances[t];e&&(delete window.hlsPlayer_instances[t],e.video.pause(),e.hls.destroy())},window.bridgeInvokeCallback=function(t,e){const s=window.bridgeCallbackMap[t];s&&s(e)}})(); \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/HlsBundle/index.html b/submodules/TelegramUniversalVideoContent/HlsBundle/index.html new file mode 100644 index 00000000000..73cff466df2 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/HlsBundle/index.html @@ -0,0 +1 @@ +Production \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore b/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore new file mode 100644 index 00000000000..fcd6fcc3a83 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore @@ -0,0 +1,28 @@ +# Node modules +node_modules/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Webpack and build artifacts +/dist +/build + +# Environment files +.env +.env.local +.env.*.local + +# OS generated +.DS_Store +Thumbs.db diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json b/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json new file mode 100644 index 00000000000..b8d2d8476e1 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json @@ -0,0 +1,8 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + ] +} \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh b/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh new file mode 100644 index 00000000000..0fbfb5b6377 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +rm -rf ../HlsBundle +mkdir ../HlsBundle +npm run build-$1 +cp ./dist/* ../HlsBundle/ diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json b/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json new file mode 100644 index 00000000000..6cb6a5f8b85 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json @@ -0,0 +1,4461 @@ +{ + "name": "myhls", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "myhls", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "hls.js": "^1.5.15" + }, + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "expose-loader": "^5.0.0", + "express": "^4.18.2", + "html-webpack-plugin": "^5.5.3", + "style-loader": "^3.3.3", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-middleware": "^6.1.1", + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^6.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001666", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", + "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.31", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", + "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expose-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", + "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hls.js": { + "version": "1.5.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.15.tgz", + "integrity": "sha512-6cD7xN6bycBHaXz2WyPIaHn/iXFizE5au2yvY5q9aC4wfihxAr16C9fUy4nxh2a3wOw0fEgLRa9dN6wsYjlpNg==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-cli/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/package.json b/submodules/TelegramUniversalVideoContent/PlayerSource/package.json new file mode 100644 index 00000000000..00b6d560911 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/package.json @@ -0,0 +1,30 @@ +{ + "name": "myhls", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build-development": "webpack --config webpack.dev.js", + "build-release": "webpack --config webpack.prod.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "expose-loader": "^5.0.0", + "express": "^4.18.2", + "html-webpack-plugin": "^5.5.3", + "style-loader": "^3.3.3", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-middleware": "^6.1.1", + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^6.0.1" + }, + "dependencies": { + "hls.js": "^1.5.15" + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/server.js b/submodules/TelegramUniversalVideoContent/PlayerSource/server.js new file mode 100644 index 00000000000..66f8abbd633 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/server.js @@ -0,0 +1,20 @@ +const express = require('express'); +const webpack = require('webpack'); +const webpackDevMiddleware = require('webpack-dev-middleware'); + +const app = express(); +const config = require('./webpack.config.js'); +const compiler = webpack(config); + +// Tell express to use the webpack-dev-middleware and use the webpack.config.js +// configuration file as a base. +app.use( + webpackDevMiddleware(compiler, { + publicPath: config.output.publicPath, + }) +); + +// Serve the files on port 3000. +app.listen(3000, function () { + console.log('Example app listening on port 3000!\n'); +}); diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js new file mode 100644 index 00000000000..b12a553a63c --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js @@ -0,0 +1,240 @@ +import { TimeRangesStub } from "./TimeRangesStub.js" + +function bytesToBase64(bytes) { + const binString = Array.from(bytes, (byte) => + String.fromCodePoint(byte), + ).join(""); + return btoa(binString); +} + +export class SourceBufferListStub extends EventTarget { + constructor() { + super(); + this._buffers = []; + } + + _add(buffer) { + this._buffers.push(buffer); + this.dispatchEvent(new Event('addsourcebuffer')); + } + + _remove(buffer) { + const index = this._buffers.indexOf(buffer); + if (index === -1) { + return false; + } + this._buffers.splice(index, 1); + this.dispatchEvent(new Event('removesourcebuffer')); + return true; + } + + get length() { + return this._buffers.length; + } + + item(index) { + return this._buffers[index]; + } + + [Symbol.iterator]() { + return this._buffers[Symbol.iterator](); + } +} + +export class SourceBufferStub extends EventTarget { + constructor(mediaSource, mimeType) { + super(); + this.mediaSource = mediaSource; + this.mimeType = mimeType; + this.updating = false; + this.buffered = new TimeRangesStub(); + this.timestampOffset = 0; + this.appendWindowStart = 0; + this.appendWindowEnd = Infinity; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "constructor", { + "mediaSourceId": this.mediaSource.bridgeId, + "mimeType": mimeType + }); + } + + appendBuffer(data) { + if (this.updating) { + throw new DOMException('SourceBuffer is updating', 'InvalidStateError'); + } + this.updating = true; + this.dispatchEvent(new Event('updatestart')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "appendBuffer", { + "data": bytesToBase64(data) + }).then((result) => { + const updatedRanges = result["ranges"]; + var ranges = []; + for (var i = 0; i < updatedRanges.length; i += 2) { + ranges.push({ + start: updatedRanges[i], + end: updatedRanges[i + 1] + }); + } + this.buffered._ranges = ranges; + + this.mediaSource._reopen(); + this.mediaSource.emitUpdatedBuffer(); + + this.updating = false; + this.dispatchEvent(new Event('update')); + this.dispatchEvent(new Event('updateend')); + }); + } + + abort() { + if (this.updating) { + this.updating = false; + this.dispatchEvent(new Event('abort')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "abort", {}).then((result) => { + }); + } + } + + remove(start, end) { + if (this.updating) { + throw new DOMException('SourceBuffer is updating', 'InvalidStateError'); + } + this.updating = true; + this.dispatchEvent(new Event('updatestart')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "remove", { + "start": start, + "end": end + }).then((result) => { + const updatedRanges = result["ranges"]; + var ranges = []; + for (var i = 0; i < updatedRanges.length; i += 2) { + ranges.push({ + start: updatedRanges[i], + end: updatedRanges[i + 1] + }); + } + this.buffered._ranges = ranges; + + this.mediaSource._reopen(); + this.mediaSource.emitUpdatedBuffer(); + + this.updating = false; + this.dispatchEvent(new Event('update')); + this.dispatchEvent(new Event('updateend')); + }); + } +} + +export class MediaSourceStub extends EventTarget { + constructor() { + super(); + + this.internalId = window.nextInternalId; + window.nextInternalId += 1; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + this.sourceBuffers = new SourceBufferListStub(); + this.activeSourceBuffers = new SourceBufferListStub(); + this.readyState = 'closed'; + this._duration = NaN; + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "constructor", { + "id": this.internalId + }); + + // Simulate asynchronous opening of MediaSource + setTimeout(() => { + this.readyState = 'open'; + this.dispatchEvent(new Event('sourceopen')); + }, 0); + } + + static isTypeSupported(mimeType) { + // Assume all MIME types are supported in this stub + return true; + } + + emitUpdatedBuffer() { + this.dispatchEvent(new Event("bufferChanged")); + } + + getBufferedRanges() { + if (this.sourceBuffers._buffers.length != 0) { + return this.sourceBuffers._buffers[0].buffered._ranges; + } + return []; + } + + addSourceBuffer(mimeType) { + if (this.readyState !== 'open') { + throw new DOMException('MediaSource is not open', 'InvalidStateError'); + } + const sourceBuffer = new SourceBufferStub(this, mimeType); + this.sourceBuffers._add(sourceBuffer); + this.activeSourceBuffers._add(sourceBuffer); + + this.dispatchEvent(new Event("bufferChanged")); + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", { + "ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId) + }).then((result) => { + }) + + return sourceBuffer; + } + + removeSourceBuffer(sourceBuffer) { + if (!this.sourceBuffers._remove(sourceBuffer)) { + throw new DOMException('SourceBuffer not found', 'NotFoundError'); + } + this.activeSourceBuffers._remove(sourceBuffer); + + this.dispatchEvent(new Event("bufferChanged")); + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", { + "ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId) + }).then((result) => { + }) + } + + endOfStream(error) { + if (this.readyState !== 'open') { + throw new DOMException('MediaSource is not open', 'InvalidStateError'); + } + this.readyState = 'ended'; + this.dispatchEvent(new Event('sourceended')); + } + + _reopen() { + if (this.readyState !== 'open') { + this.readyState = 'open'; + this.dispatchEvent(new Event('sourceopen')); + } + } + + set duration(value) { + if (this.readyState === 'closed') { + throw new DOMException('MediaSource is closed', 'InvalidStateError'); + } + this._duration = value; + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "setDuration", { + "duration": value + }).then((result) => { + }) + } + + get duration() { + return this._duration; + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js new file mode 100644 index 00000000000..e9aebfc22ee --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js @@ -0,0 +1,85 @@ + +export class TextTrackStub extends EventTarget { + constructor(kind = '', label = '', language = '') { + super(); + this.kind = kind; + this.label = label; + this.language = language; + this.mode = 'disabled'; // 'disabled', 'hidden', or 'showing' + this.cues = new TextTrackCueListStub(); + this.activeCues = new TextTrackCueListStub(); + } + + addCue(cue) { + this.cues._add(cue); + } + + removeCue(cue) { + this.cues._remove(cue); + } +} + +export class TextTrackCueListStub { + constructor() { + this._cues = []; + } + + get length() { + return this._cues.length; + } + + item(index) { + return this._cues[index]; + } + + getCueById(id) { + return this._cues.find(cue => cue.id === id) || null; + } + + _add(cue) { + this._cues.push(cue); + } + + _remove(cue) { + const index = this._cues.indexOf(cue); + if (index !== -1) { + this._cues.splice(index, 1); + } + } + + [Symbol.iterator]() { + return this._cues[Symbol.iterator](); + } +} + +export class TextTrackListStub extends EventTarget { + constructor() { + super(); + this._tracks = []; + } + + get length() { + return this._tracks.length; + } + + item(index) { + return this._tracks[index]; + } + + _add(track) { + this._tracks.push(track); + this.dispatchEvent(new Event('addtrack')); + } + + _remove(track) { + const index = this._tracks.indexOf(track); + if (index !== -1) { + this._tracks.splice(index, 1); + this.dispatchEvent(new Event('removetrack')); + } + } + + [Symbol.iterator]() { + return this._tracks[Symbol.iterator](); + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js new file mode 100644 index 00000000000..e8a9df021e9 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js @@ -0,0 +1,74 @@ + +export class TimeRangesStub { + constructor() { + this._ranges = []; + } + + get length() { + return this._ranges.length; + } + + start(index) { + if (index < 0 || index >= this._ranges.length) { + throw new DOMException('Invalid index', 'IndexSizeError'); + } + return this._ranges[index].start; + } + + end(index) { + if (index < 0 || index >= this._ranges.length) { + throw new DOMException('Invalid index', 'IndexSizeError'); + } + return this._ranges[index].end; + } + + // Helper method to add a range + _addRange(start, end) { + this._ranges.push({ start, end }); + this._normalizeRanges(); + } + + // Helper method to remove ranges that overlap with a given range + _removeRange(start, end) { + let updatedRanges = []; + for (let range of this._ranges) { + if (range.end <= start || range.start >= end) { + // No overlap, keep the range as is + updatedRanges.push(range); + } else if (range.start < start && range.end > end) { + // The range fully covers the removal range, split into two ranges + updatedRanges.push({ start: range.start, end: start }); + updatedRanges.push({ start: end, end: range.end }); + } else if (range.start >= start && range.end <= end) { + // The range is entirely within the removal range, remove it + // Do not add to updatedRanges + } else if (range.start < start && range.end > start && range.end <= end) { + // The range overlaps with the removal range on the left + updatedRanges.push({ start: range.start, end: start }); + } else if (range.start >= start && range.start < end && range.end > end) { + // The range overlaps with the removal range on the right + updatedRanges.push({ start: end, end: range.end }); + } + } + this._ranges = updatedRanges; + } + + // Normalize and merge overlapping ranges + _normalizeRanges() { + this._ranges.sort((a, b) => a.start - b.start); + let normalized = []; + for (let range of this._ranges) { + if (normalized.length === 0) { + normalized.push(range); + } else { + let last = normalized[normalized.length - 1]; + if (range.start <= last.end) { + last.end = Math.max(last.end, range.end); + } else { + normalized.push(range); + } + } + } + this._ranges = normalized; + } +} \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js new file mode 100644 index 00000000000..cd59430c345 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js @@ -0,0 +1,196 @@ +import { TimeRangesStub } from "./TimeRangesStub.js" +import { TextTrackStub, TextTrackListStub } from "./TextTrackStub.js" + +export class VideoElementStub extends EventTarget { + constructor(id) { + super(); + + this.instanceId = id; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + this._currentTime = 0.0; + this.duration = NaN; + this.paused = true; + this._playbackRate = 1.0; + this.volume = 1.0; + this.muted = false; + this.readyState = 0; + this.networkState = 0; + this.buffered = new TimeRangesStub(); + this.seeking = false; + this.loop = false; + this.autoplay = false; + this.controls = false; + this.error = null; + this._src = ''; + this.videoWidth = 0; + this.videoHeight = 0; + this.textTracks = new TextTrackListStub(); + this.isWaiting = false; + this.currentMedia = null; + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "constructor", { + "instanceId": this.instanceId + }); + + setTimeout(() => { + this.readyState = 4; // HAVE_ENOUGH_DATA + this.dispatchEvent(new Event('loadedmetadata')); + this.dispatchEvent(new Event('loadeddata')); + this.dispatchEvent(new Event('canplay')); + this.dispatchEvent(new Event('canplaythrough')); + }, 0); + } + + get currentTime() { + return this._currentTime; + } + + set currentTime(value) { + if (this._currentTime != value) { + this._currentTime = value; + + this.dispatchEvent(new Event('seeking')); + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setCurrentTime", { + "instanceId": this.instanceId, + "currentTime": value + }).then((result) => { + this.dispatchEvent(new Event('seeked')); + }) + } + } + + get playbackRate() { + return this._playbackRate; + } + + set playbackRate(value) { + this._playbackRate = value; + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setPlaybackRate", { + "instanceId": this.instanceId, + "playbackRate": value + }).then((result) => { + }) + } + + get src() { + return this._src; + } + + set src(value) { + if (this.currentMedia) { + this.currentMedia.removeEventListener("bufferChanged", false); + } + + this._src = value; + var media = window.mediaSourceMap[this._src]; + this.currentMedia = media; + if (media) { + media.addEventListener("bufferChanged", () => { + this.updateBufferedFromMediaSource(); + }, false); + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setMediaSource", { + "instanceId": this.instanceId, + "mediaSourceId": media.bridgeId + }).then((result) => { + }) + } + } + + removeAttribute(name) { + if (name === "src") { + this._src = ""; + } + } + + querySelectorAll(name) { + const fragment = document.createDocumentFragment(); + return fragment.querySelectorAll('*'); + } + + updateBufferedFromMediaSource() { + var currentMedia = this.currentMedia; + if (currentMedia) { + this.buffered._ranges = currentMedia.getBufferedRanges(); + } else { + this.buffered._ranges = []; + } + } + + bridgeUpdateStatus(dict) { + var paused = !dict["isPlaying"]; + var isWaiting = dict["isWaiting"]; + var currentTime = dict["currentTime"]; + + if (this.paused != paused) { + this.paused = paused; + + if (paused) { + this.dispatchEvent(new Event('pause')); + } else { + this.dispatchEvent(new Event('play')); + this.dispatchEvent(new Event('playing')); + } + } + + if (this.isWaiting != isWaiting) { + this.isWaiting = isWaiting; + if (isWaiting) { + this.dispatchEvent(new Event('waiting')); + } + } + + if (this._currentTime != currentTime) { + this._currentTime = currentTime; + this.dispatchEvent(new Event('timeupdate')); + } + } + + play() { + if (this.paused) { + return window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "play", { + "instanceId": this.instanceId, + }).then((result) => { + this.dispatchEvent(new Event('play')); + this.dispatchEvent(new Event('playing')); + }) + } else { + return Promise.resolve(); + } + } + + pause() { + if (!this.paused) { + this.paused = true; + this.dispatchEvent(new Event('pause')); + + return window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "pause", { + "instanceId": this.instanceId, + }).then((result) => { + }) + } + } + + canPlayType(type) { + return 'probably'; + } + + addTextTrack(kind, label, language) { + const textTrack = new TextTrackStub(kind, label, language); + this.textTracks._add(textTrack); + return textTrack; + } + + load() { + } + + notifySeeked() { + this.dispatchEvent(new Event('seeking')); + this.dispatchEvent(new Event('seeked')); + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js new file mode 100644 index 00000000000..c79dd489bbd --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js @@ -0,0 +1,150 @@ + +function base64ToArrayBuffer(base64) { + var binaryString = atob(base64); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +export class XMLHttpRequestStub extends EventTarget { + constructor() { + super(); + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + + this.readyState = 0; + this.status = 0; + this.statusText = ""; + this.responseText = ""; + this.responseXML = null; + this._responseData = null; + this.onreadystatechange = null; + this._requestHeaders = {}; + this._responseHeaders = {}; + this._method = ""; + this._url = ""; + this._async = true; + this._user = null; + this._password = null; + this._responseType = ""; + } + + open(method, url, async = true, user = null, password = null) { + this._method = method; + this._url = url; + this._async = async; + this._user = user; + this._password = password; + this.readyState = 1; // Opened + this._triggerReadyStateChange(); + } + + setRequestHeader(header, value) { + this._requestHeaders[header] = value; + } + + getResponseHeader(header) { + return this._responseHeaders[header.toLowerCase()] || null; + } + + getAllResponseHeaders() { + return Object.entries(this._responseHeaders) + .map(([header, value]) => `${header}: ${value}`) + .join('\r\n'); + } + + send(body = null) { + this.readyState = 2; + this._triggerReadyStateChange(); + + this.readyState = 3; // Loading + this._triggerReadyStateChange(); + + this.dispatchEvent(new Event("loadstart")); + + window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "load", { + "id": this.bridgeId, + "url": this._url, + "requestHeaders": this._requestHeaders + }).then((result) => { + if (result["error"]) { + this.dispatchEvent(new Event("error")); + } else { + this.status = result["status"]; + this.statusText = result["statusText"]; + + if (result["responseData"]) { + if (this._responseType === "arraybuffer") { + this._responseData = base64ToArrayBuffer(result["responseData"]); + } else { + this.responseText = atob(result["responseData"]); + } + this.responseXML = null; + } else { + this.response = null; + this.responseText = result["responseText"] || null; + this.responseXML = result["responseXML"] || null; + } + this._responseHeaders = result["responseHeaders"]; + + this.readyState = 4; // Done + this._triggerReadyStateChange(); + + this.dispatchEvent(new Event("load")); + } + + this.dispatchEvent(new Event("loadend")); + }); + } + + abort() { + this.dispatchEvent(new Event("abort")); + + window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "abort", { + "id": this.bridgeId + }); + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.responseXML = null; + this._responseHeaders = {}; + this._triggerReadyStateChange(); + } + + overrideMimeType(mime) { + } + + set responseType(type) { + this._responseType = type; + } + + get responseType() { + return this._responseType; + } + + get response() { + if (this._responseType === '' || this._responseType === 'text') { + return this.responseText; + } + return this._responseData; + } + + _triggerReadyStateChange() { + this.dispatchEvent(new Event('readystatechange')); + if (typeof this.onreadystatechange === 'function') { + this.onreadystatechange(); + } + } + + // Additional methods to simulate responses + _setResponse(status, statusText, responseText, responseHeaders = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this._responseHeaders = responseHeaders; + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js new file mode 100644 index 00000000000..1df18a87b05 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js @@ -0,0 +1,238 @@ +import Hls from "hls.js"; +import { VideoElementStub } from "./VideoElementStub.js" +import { MediaSourceStub, SourceBufferStub } from "./MediaSourceStub.js" +import { XMLHttpRequestStub } from "./XMLHttpRequestStub.js" + +window.bridgeObjectMap = {}; +window.bridgeCallbackMap = {}; + +function bridgeInvokeAsync(bridgeId, className, methodName, params) { + var promiseResolve; + var promiseReject; + var result = new Promise(function(resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + const callbackId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeCallbackMap[callbackId] = promiseResolve; + + if (window.webkit.messageHandlers) { + window.webkit.messageHandlers.performAction.postMessage({ + 'event': 'bridgeInvoke', + 'data': { + 'bridgeId': bridgeId, + 'className': className, + 'methodName': methodName, + 'params': params, + 'callbackId': callbackId + } + }); + } + + return result; +} +window.bridgeInvokeAsync = bridgeInvokeAsync + +export function bridgeInvokeCallback(callbackId, result) { + const callback = window.bridgeCallbackMap[callbackId]; + if (callback) { + callback(result); + } +} + +window.nextInternalId = 0; +window.mediaSourceMap = {}; + +// Replace the global MediaSource with our stub +if (typeof window !== 'undefined') { + window.MediaSource = MediaSourceStub; + window.ManagedMediaSource = MediaSourceStub; + window.SourceBuffer = SourceBufferStub; + window.XMLHttpRequest = XMLHttpRequestStub; + + URL.createObjectURL = function(ms) { + const url = "blob:mock-media-source:" + ms.internalId; + window.mediaSourceMap[url] = ms; + return url; + }; +} + +function postPlayerEvent(id, eventName, eventData) { + if (window.webkit && window.webkit.messageHandlers) { + window.webkit.messageHandlers.performAction.postMessage({'instanceId': id, 'event': eventName, 'data': eventData}); + } +} + +export class HlsPlayerInstance { + constructor(id) { + this.id = id; + this.isManifestParsed = false; + this.currentTimeUpdateTimeout = null; + this.notifySeekedOnNextStatusUpdate = false; + this.video = new VideoElementStub(this.id); + } + + playerInitialize(params) { + this.video.addEventListener("playing", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("pause", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("seeking", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("waiting", () => { + this.refreshPlayerStatus(); + }); + + this.hls = new Hls({ + startLevel: 0, + testBandwidth: false, + debug: params['debug'] || true, + autoStartLoad: false, + backBufferLength: 30, + maxBufferLength: 60, + maxMaxBufferLength: 60, + maxFragLookUpTolerance: 0.001, + nudgeMaxRetry: 10000 + }); + this.hls.on(Hls.Events.MANIFEST_PARSED, () => { + this.isManifestParsed = true; + this.refreshPlayerStatus(); + }); + + this.hls.on(Hls.Events.LEVEL_SWITCHED, () => { + this.refreshPlayerStatus(); + }); + this.hls.on(Hls.Events.LEVELS_UPDATED, () => { + this.refreshPlayerStatus(); + }); + + this.hls.loadSource(params["urlPrefix"] + "master.m3u8"); + this.hls.attachMedia(this.video); + } + + playerLoad(initialLevelIndex) { + this.hls.startLevel = initialLevelIndex; + this.hls.startLoad(-1, false); + } + + playerPlay() { + this.video.play(); + } + + playerPause() { + this.video.pause(); + } + + playerSetBaseRate(value) { + this.video.playbackRate = value; + } + + playerSetLevel(level) { + if (level >= 0) { + this.hls.currentLevel = level; + } else { + this.hls.currentLevel = -1; + } + } + + playerSeek(value) { + this.video.currentTime = value; + } + + playerSetIsMuted(value) { + this.video.muted = value; + } + + getLevels() { + var levels = []; + for (var i = 0; i < this.hls.levels.length; i++) { + var level = this.hls.levels[i]; + levels.push({ + 'index': i, + 'bitrate': level.bitrate || 0, + 'width': level.width || 0, + 'height': level.height || 0 + }); + } + return levels; + } + + refreshPlayerStatus() { + var isPlaying = false; + if (!this.video.paused && !this.video.ended && this.video.readyState > 2) { + isPlaying = true; + } + + postPlayerEvent(this.id, 'playerStatus', { + 'isReady': this.isManifestParsed, + 'isPlaying': !this.video.paused, + 'rate': isPlaying ? this.video.playbackRate : 0.0, + 'defaultRate': this.video.playbackRate, + 'levels': this.getLevels(), + 'currentLevel': this.hls.currentLevel + }); + + this.refreshPlayerCurrentTime(); + + if (isPlaying) { + if (this.currentTimeUpdateTimeout == null) { + this.currentTimeUpdateTimeout = setTimeout(() => { + this.refreshPlayerCurrentTime(); + }, 200); + } + } else { + if(this.currentTimeUpdateTimeout != null){ + clearTimeout(this.currentTimeUpdateTimeout); + this.currentTimeUpdateTimeout = null; + } + } + + if (this.notifySeekedOnNextStatusUpdate) { + this.notifySeekedOnNextStatusUpdate = false; + this.video.notifySeeked(); + } + } + + playerNotifySeekedOnNextStatusUpdate() { + this.notifySeekedOnNextStatusUpdate = true; + } + + refreshPlayerCurrentTime() { + postPlayerEvent(this.id, 'playerCurrentTime', { + 'value': this.video.currentTime + }); + this.currentTimeUpdateTimeout = setTimeout(() => { + this.refreshPlayerCurrentTime() + }, 200); + } +} + +window.invokeOnLoad = function() { + postPlayerEvent(this.id, 'windowOnLoad', { + }); +} + +window.onload = () => { + window.invokeOnLoad(); +}; + +window.hlsPlayer_instances = {}; + +window.hlsPlayer_makeInstance = function(id) { + window.hlsPlayer_instances[id] = new HlsPlayerInstance(id); +} + +window.hlsPlayer_destroyInstance = function(id) { + const instance = window.hlsPlayer_instances[id]; + if (instance) { + delete window.hlsPlayer_instances[id]; + instance.video.pause(); + instance.hls.destroy(); + } +} + +window.bridgeInvokeCallback = bridgeInvokeCallback; diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css b/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css new file mode 100644 index 00000000000..4cf22426dc3 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css @@ -0,0 +1,15 @@ +html, body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; +} +video { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + object-fit: fill; +} + diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js new file mode 100644 index 00000000000..a8d8d45fc76 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js @@ -0,0 +1,35 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: { + index: './src/index.js', + }, + plugins: [ + new HtmlWebpackPlugin({ + title: 'Production', + scriptLoading: 'blocking', + }) + ], + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'dist'), + clean: true, + publicPath: '', + }, + module: { + rules: [ + { + test: /\.js$/, + include: path.resolve(__dirname, 'src/index.js'), + }, + { + test: /\.css$/i, + use: [ + 'style-loader', + 'css-loader' + ], + }, + ], + }, +}; diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js new file mode 100644 index 00000000000..bf02fafa868 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js @@ -0,0 +1,10 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'development', + devtool: 'source-map', + devServer: { + static: './dist', + }, +}); diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js new file mode 100644 index 00000000000..6266c6035fb --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js @@ -0,0 +1,15 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); +const TerserPlugin = require('terser-webpack-plugin'); + +module.exports = merge(common, { + mode: 'production', + optimization: { + minimize: true, + minimizer: [new TerserPlugin({ + terserOptions: { + compress: true, + }, + })], + }, +}); diff --git a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift index e80b23197a4..ca6b841c481 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift @@ -16,7 +16,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private let inset: CGFloat - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init(inset: CGFloat, backgroundImage: UIImage?, tapped: @escaping () -> Void) { self.inset = inset @@ -51,9 +51,9 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -63,8 +63,8 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let diameter = size.width + inset self.contentContainerNode.cornerRadius = (diameter - 3.0) / 2.0 @@ -80,7 +80,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { self.contentContainerNode.subnodeTransform = CATransform3DMakeScale((contentFrame.width + 2.0) / contentFrame.width, (contentFrame.width + 2.0) / contentFrame.width, 1.0) if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift index 46b641a0e2f..30bcc375aaa 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift @@ -23,7 +23,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init(corners: ImageCorners, nativeSize: CGSize, contentMode: ChatBubbleVideoDecorationContentMode, backgroundColor: UIColor) { self.corners = corners @@ -82,23 +82,23 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let size = self.validLayoutSize { + if let validLayout = self.validLayout { var scaledSize: CGSize switch self.contentMode { case .aspectFit: - scaledSize = self.nativeSize.aspectFitted(size) + scaledSize = self.nativeSize.aspectFitted(validLayout.size) case .aspectFill: - scaledSize = self.nativeSize.aspectFilled(size) + scaledSize = self.nativeSize.aspectFilled(validLayout.size) } - if abs(scaledSize.width - size.width) < 2.0 { - scaledSize.width = size.width + if abs(scaledSize.width - validLayout.size.width) < 2.0 { + scaledSize.width = validLayout.size.width } - if abs(scaledSize.height - size.height) < 2.0 { - scaledSize.height = size.height + if abs(scaledSize.height - validLayout.size.height) < 2.0 { + scaledSize.height = validLayout.size.height } - contentNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize) - contentNode.updateLayout(size: scaledSize, transition: .immediate) + contentNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.size.width - scaledSize.width) / 2.0), y: floor((validLayout.size.height - scaledSize.height) / 2.0)), size: scaledSize) + contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: .immediate) } } } @@ -108,8 +108,8 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -137,7 +137,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { scaledSize.height = size.height } transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) - contentNode.updateLayout(size: scaledSize, transition: transition) + contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: transition) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift new file mode 100644 index 00000000000..27942f60500 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AVFoundation +import UniversalMediaPlayer +import TelegramAudio +import AccountContext +import PhotoResources +import RangeSet +import TelegramVoip +import ManagedFile +import AppBundle + +public final class HLSQualitySet { + public let qualityFiles: [Int: FileMediaReference] + public let playlistFiles: [Int: FileMediaReference] + + public init?(baseFile: FileMediaReference) { + var qualityFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in baseFile.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + for attribute in alternativeFile.attributes { + if case let .Video(_, size, _, _, _, videoCodec) = attribute { + if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { + let key = Int(min(size.width, size.height)) + if let currentFile = qualityFiles[key] { + var currentCodec: String? + for attribute in currentFile.media.attributes { + if case let .Video(_, _, _, _, _, videoCodec) = attribute { + currentCodec = videoCodec + } + } + if let currentCodec, currentCodec == "av1" { + } else { + qualityFiles[key] = baseFile.withMedia(alternativeFile) + } + } else { + qualityFiles[key] = baseFile.withMedia(alternativeFile) + } + } + } + } + } + } + + var playlistFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in baseFile.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + if alternativeFile.mimeType == "application/x-mpegurl" { + if let fileName = alternativeFile.fileName { + if fileName.hasPrefix("mtproto:") { + let fileIdString = String(fileName[fileName.index(fileName.startIndex, offsetBy: "mtproto:".count)...]) + if let fileId = Int64(fileIdString) { + for (quality, file) in qualityFiles { + if file.media.fileId.id == fileId { + playlistFiles[quality] = baseFile.withMedia(alternativeFile) + break + } + } + } + } + } + } + } + } + if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys { + self.qualityFiles = qualityFiles + self.playlistFiles = playlistFiles + } else { + return nil + } + } +} + +public final class HLSVideoContent: UniversalVideoContent { + public static func minimizedHLSQuality(file: FileMediaReference) -> (playlist: FileMediaReference, file: FileMediaReference)? { + guard let qualitySet = HLSQualitySet(baseFile: file) else { + return nil + } + for (quality, qualityFile) in qualitySet.qualityFiles.sorted(by: { $0.key < $1.key }) { + if quality >= 400 { + guard let playlistFile = qualitySet.playlistFiles[quality] else { + return nil + } + return (playlistFile, qualityFile) + } + } + return nil + } + + public static func minimizedHLSQualityPreloadData(postbox: Postbox, file: FileMediaReference, userLocation: MediaResourceUserLocation, prefixSeconds: Int, autofetchPlaylist: Bool) -> Signal<(FileMediaReference, Range)?, NoError> { + guard let fileSet = minimizedHLSQuality(file: file) else { + return .single(nil) + } + + let playlistData: Signal?, NoError> = Signal { subscriber in + var fetchDisposable: Disposable? + if autofetchPlaylist { + fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: fileSet.playlist, resource: fileSet.playlist.media.resource).start() + } + let dataDisposable = postbox.mediaBox.resourceData(fileSet.playlist.media.resource).start(next: { data in + if !data.complete { + return + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + guard let playlistString = String(data: data, encoding: .utf8) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + var durations: [Int] = [] + var byteRanges: [Range] = [] + + let extinfRegex = try! NSRegularExpression(pattern: "EXTINF:(\\d+)", options: []) + let byteRangeRegex = try! NSRegularExpression(pattern: "EXT-X-BYTERANGE:(\\d+)@(\\d+)", options: []) + + let extinfResults = extinfRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in extinfResults { + if let durationRange = Range(result.range(at: 1), in: playlistString) { + if let duration = Int(String(playlistString[durationRange])) { + durations.append(duration) + } + } + } + + let byteRangeResults = byteRangeRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in byteRangeResults { + if let lengthRange = Range(result.range(at: 1), in: playlistString), let upperBoundRange = Range(result.range(at: 2), in: playlistString) { + if let length = Int(String(playlistString[lengthRange])), let lowerBound = Int(String(playlistString[upperBoundRange])) { + byteRanges.append(lowerBound ..< (lowerBound + length)) + } + } + } + + if durations.count != byteRanges.count { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + var rangeUpperBound: Int64 = 0 + var remainingSeconds = prefixSeconds + + for i in 0 ..< durations.count { + if remainingSeconds <= 0 { + break + } + let duration = durations[i] + let byteRange = byteRanges[i] + + remainingSeconds -= duration + rangeUpperBound = max(rangeUpperBound, Int64(byteRange.upperBound)) + } + + if rangeUpperBound != 0 { + subscriber.putNext(0 ..< rangeUpperBound) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + + return + }) + + return ActionDisposable { + fetchDisposable?.dispose() + dataDisposable.dispose() + } + } + + return playlistData + |> map { range -> (FileMediaReference, Range)? in + guard let range else { + return nil + } + return (fileSet.file, range) + } + } + + public let id: AnyHashable + public let nativeId: PlatformVideoContentId + let userLocation: MediaResourceUserLocation + public let fileReference: FileMediaReference + public let dimensions: CGSize + public let duration: Double + let streamVideo: Bool + let loopVideo: Bool + let enableSound: Bool + let baseRate: Double + let fetchAutomatically: Bool + + public init(id: PlatformVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { + self.id = id + self.userLocation = userLocation + self.nativeId = id + self.fileReference = fileReference + self.dimensions = self.fileReference.media.dimensions?.cgSize ?? CGSize(width: 480, height: 320) + self.duration = self.fileReference.media.duration ?? 0.0 + self.streamVideo = streamVideo + self.loopVideo = loopVideo + self.enableSound = enableSound + self.baseRate = baseRate + self.fetchAutomatically = fetchAutomatically + } + + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return HLSVideoJSNativeContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + } + + public func isEqual(to other: UniversalVideoContent) -> Bool { + if let other = other as? HLSVideoContent { + if case let .message(_, stableId, _) = self.nativeId { + if case .message(_, stableId, _) = other.nativeId { + if self.fileReference.media.isInstantVideo { + return true + } + } + } + } + return false + } +} + +final class HLSServerSource: SharedHLSServer.Source { + let id: String + let postbox: Postbox + let userLocation: MediaResourceUserLocation + let playlistFiles: [Int: FileMediaReference] + let qualityFiles: [Int: FileMediaReference] + + private var playlistFetchDisposables: [Int: Disposable] = [:] + + init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" + self.postbox = postbox + self.userLocation = userLocation + self.playlistFiles = playlistFiles + self.qualityFiles = qualityFiles + } + + deinit { + for (_, disposable) in self.playlistFetchDisposables { + disposable.dispose() + } + } + + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return Signal { subscriber in + if path == "index.html" { + if let path = getAppBundle().path(forResource: "HLSVideoPlayer", ofType: "html"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + subscriber.putNext((data, "text/html")) + } else { + subscriber.putNext(nil) + } + } else if path == "hls.js" { + if let path = getAppBundle().path(forResource: "hls", ofType: "js"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + subscriber.putNext((data, "application/javascript")) + } else { + subscriber.putNext(nil) + } + } else { + subscriber.putNext(nil) + } + + subscriber.putCompletion() + + return EmptyDisposable + } + } + + func masterPlaylistData() -> Signal { + var playlistString: String = "" + playlistString.append("#EXTM3U\n") + + for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { + let width = file.media.dimensions?.width ?? 1280 + let height = file.media.dimensions?.height ?? 720 + + let bandwidth: Int + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + bandwidth = Int(Double(size) / duration) * 8 + } else { + bandwidth = 1000000 + } + + playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") + playlistString.append("hls_level_\(quality).m3u8\n") + } + return .single(playlistString) + } + + func playlistData(quality: Int) -> Signal { + guard let playlistFile = self.playlistFiles[quality] else { + return .never() + } + if self.playlistFetchDisposables[quality] == nil { + self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() + } + + return self.postbox.mediaBox.resourceData(playlistFile.media.resource) + |> filter { data in + return data.complete + } + |> map { data -> String in + guard data.complete else { + return "" + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return "" + } + guard var playlistString = String(data: data, encoding: .utf8) else { + return "" + } + let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) + let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in results.reversed() { + if let range = Range(result.range, in: playlistString) { + if let fileIdRange = Range(result.range(at: 1), in: playlistString) { + let fileId = String(playlistString[fileIdRange]) + playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") + } + } + } + return playlistString + } + } + + func partData(index: Int, quality: Int) -> Signal { + return .never() + } + + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { + return .single(nil) + } + let _ = quality + guard let size = file.media.size else { + return .single(nil) + } + + let postbox = self.postbox + let userLocation = self.userLocation + + let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) + + let queue = postbox.mediaBox.dataQueue + let fetchFromRemote: Signal<(TempBoxFile, Range, Int)?, NoError> = Signal { subscriber in + let partialFile = TempBox.shared.tempFile(fileName: "data") + + if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) { + #if DEBUG + print("Fetched \(quality)p part from cache") + #endif + + let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite) + if let outputFile { + let blockSize = 128 * 1024 + var tempBuffer = Data(count: blockSize) + var blockOffset = 0 + while blockOffset < cachedData.length { + let currentBlockSize = min(cachedData.length - blockOffset, blockSize) + + tempBuffer.withUnsafeMutableBytes { bytes -> Void in + let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize) + let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize) + } + + blockOffset += blockSize + } + outputFile._unsafeClose() + subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size))) + subscriber.putCompletion() + } else { + #if DEBUG + print("Error writing cached file to disk") + #endif + } + + return EmptyDisposable + } + + guard let fetchResource = postbox.mediaBox.fetchResource else { + return EmptyDisposable + } + + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) + let params = MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), + info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), + location: location, + contentType: .video, + isRandomAccessAllowed: true + ) + + let completeFile = TempBox.shared.tempFile(fileName: "data") + let metaFile = TempBox.shared.tempFile(fileName: "data") + + guard let fileContext = MediaBoxFileContextV2Impl( + queue: queue, + manager: postbox.mediaBox.dataFileManager, + storageBox: nil, + resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, + path: completeFile.path, + partialPath: partialFile.path, + metaPath: metaFile.path + ) else { + return EmptyDisposable + } + + let fetchDisposable = fileContext.fetched( + range: mappedRange, + priority: .default, + fetch: { intervals in + return fetchResource(file.media.resource, intervals, params) + }, + error: { _ in + }, + completed: { + } + ) + + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + + let dataDisposable = fileContext.data( + range: mappedRange, + waitUntilAfterInitialFetch: true, + next: { result in + if result.complete { + #if DEBUG + let fetchTime = CFAbsoluteTimeGetCurrent() - startTime + print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") + #endif + subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) + subscriber.putCompletion() + } + } + ) + + return ActionDisposable { + queue.async { + fetchDisposable.dispose() + dataDisposable.dispose() + fileContext.cancelFullRangeFetches() + + TempBox.shared.dispose(completeFile) + TempBox.shared.dispose(metaFile) + } + } + } + |> runOn(queue) + + return fetchFromRemote + } +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift new file mode 100644 index 00000000000..9d89d4112b8 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -0,0 +1,1847 @@ +import Foundation +import AVFoundation +import SwiftSignalKit +import UniversalMediaPlayer +import Postbox +import TelegramCore +import WebKit +import AsyncDisplayKit +import AccountContext +import TelegramAudio +import Display +import PhotoResources +import TelegramVoip +import RangeSet +import AppBundle +import ManagedFile +import FFMpegBinding +import RangeSet + +private func parseRange(from rangeString: String) -> Range? { + guard rangeString.hasPrefix("bytes=") else { + return nil + } + + let rangeValues = rangeString.dropFirst("bytes=".count).split(separator: "-") + + guard rangeValues.count == 2, + let start = Int(rangeValues[0]), + let end = Int(rangeValues[1]) else { + return nil + } + return start ..< end +} + +final class HLSJSServerSource: SharedHLSServer.Source { + let id: String + let postbox: Postbox + let userLocation: MediaResourceUserLocation + let playlistFiles: [Int: FileMediaReference] + let qualityFiles: [Int: FileMediaReference] + + private var playlistFetchDisposables: [Int: Disposable] = [:] + + init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" + self.postbox = postbox + self.userLocation = userLocation + self.playlistFiles = playlistFiles + self.qualityFiles = qualityFiles + } + + deinit { + for (_, disposable) in self.playlistFetchDisposables { + disposable.dispose() + } + } + + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return Signal { subscriber in + let bundle = Bundle(for: HLSJSServerSource.self) + + let bundlePath = bundle.bundlePath + "/HlsBundle.bundle" + if let data = try? Data(contentsOf: URL(fileURLWithPath: bundlePath + "/" + path)) { + let mimeType: String + let pathExtension = (path as NSString).pathExtension + if pathExtension == "html" { + mimeType = "text/html" + } else if pathExtension == "html" { + mimeType = "application/javascript" + } else { + mimeType = "application/octet-stream" + } + subscriber.putNext((data, mimeType)) + } else { + subscriber.putNext(nil) + } + + subscriber.putCompletion() + + return EmptyDisposable + } + } + + func masterPlaylistData() -> Signal { + var playlistString: String = "" + playlistString.append("#EXTM3U\n") + + for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { + let width = file.media.dimensions?.width ?? 1280 + let height = file.media.dimensions?.height ?? 720 + + let bandwidth: Int + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + bandwidth = Int(Double(size) / duration) * 8 + } else { + bandwidth = 1000000 + } + + playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") + playlistString.append("hls_level_\(quality).m3u8\n") + } + return .single(playlistString) + } + + func playlistData(quality: Int) -> Signal { + guard let playlistFile = self.playlistFiles[quality] else { + return .never() + } + if self.playlistFetchDisposables[quality] == nil { + self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() + } + + return self.postbox.mediaBox.resourceData(playlistFile.media.resource) + |> filter { data in + return data.complete + } + |> map { data -> String in + guard data.complete else { + return "" + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return "" + } + guard var playlistString = String(data: data, encoding: .utf8) else { + return "" + } + let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) + let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in results.reversed() { + if let range = Range(result.range, in: playlistString) { + if let fileIdRange = Range(result.range(at: 1), in: playlistString) { + let fileId = String(playlistString[fileIdRange]) + playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") + } + } + } + return playlistString + } + } + + func partData(index: Int, quality: Int) -> Signal { + return .never() + } + + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { + return .single(nil) + } + let _ = quality + guard let size = file.media.size else { + return .single(nil) + } + + let postbox = self.postbox + let userLocation = self.userLocation + + let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) + + let queue = postbox.mediaBox.dataQueue + let fetchFromRemote: Signal<(TempBoxFile, Range, Int)?, NoError> = Signal { subscriber in + let partialFile = TempBox.shared.tempFile(fileName: "data") + + if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) { + #if DEBUG + print("Fetched \(quality)p part from cache") + #endif + + let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite) + if let outputFile { + let blockSize = 128 * 1024 + var tempBuffer = Data(count: blockSize) + var blockOffset = 0 + while blockOffset < cachedData.length { + let currentBlockSize = min(cachedData.length - blockOffset, blockSize) + + tempBuffer.withUnsafeMutableBytes { bytes -> Void in + let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize) + let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize) + } + + blockOffset += blockSize + } + outputFile._unsafeClose() + subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size))) + subscriber.putCompletion() + } else { + #if DEBUG + print("Error writing cached file to disk") + #endif + } + + return EmptyDisposable + } + + guard let fetchResource = postbox.mediaBox.fetchResource else { + return EmptyDisposable + } + + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) + let params = MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), + info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), + location: location, + contentType: .video, + isRandomAccessAllowed: true + ) + + let completeFile = TempBox.shared.tempFile(fileName: "data") + let metaFile = TempBox.shared.tempFile(fileName: "data") + + guard let fileContext = MediaBoxFileContextV2Impl( + queue: queue, + manager: postbox.mediaBox.dataFileManager, + storageBox: nil, + resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, + path: completeFile.path, + partialPath: partialFile.path, + metaPath: metaFile.path + ) else { + return EmptyDisposable + } + + let fetchDisposable = fileContext.fetched( + range: mappedRange, + priority: .default, + fetch: { intervals in + return fetchResource(file.media.resource, intervals, params) + }, + error: { _ in + }, + completed: { + } + ) + + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + + let dataDisposable = fileContext.data( + range: mappedRange, + waitUntilAfterInitialFetch: true, + next: { result in + if result.complete { + #if DEBUG + let fetchTime = CFAbsoluteTimeGetCurrent() - startTime + print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") + #endif + subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) + subscriber.putCompletion() + } + } + ) + + return ActionDisposable { + queue.async { + fetchDisposable.dispose() + dataDisposable.dispose() + fileContext.cancelFullRangeFetches() + + TempBox.shared.dispose(completeFile) + TempBox.shared.dispose(metaFile) + } + } + } + |> runOn(queue) + + return fetchFromRemote + } +} + +private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + +private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate { + private final class ContextReference { + weak var contentNode: HLSVideoJSNativeContentNode? + + init(contentNode: HLSVideoJSNativeContentNode?) { + self.contentNode = contentNode + } + } + + private enum ResponseError { + case badRequest + case notFound + case internalServerError + + var httpStatus: (Int, String) { + switch self { + case .badRequest: + return (400, "Bad Request") + case .notFound: + return (404, "Not Found") + case .internalServerError: + return (500, "Internal Server Error") + } + } + } + + static let shared: SharedHLSVideoWebView = SharedHLSVideoWebView() + + private var contextReferences: [Int: ContextReference] = [:] + + var webView: WKWebView? + + var videoElements: [Int: VideoElement] = [:] + var mediaSources: [Int: MediaSource] = [:] + var sourceBuffers: [Int: SourceBuffer] = [:] + + private var isWebViewReady: Bool = false + private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = [] + + private var tempTasks: [Int: URLSessionTask] = [:] + + private var emptyTimer: Foundation.Timer? + + override init() { + super.init() + } + + deinit { + self.emptyTimer?.invalidate() + } + + private func createWebView() { + let config = WKWebViewConfiguration() + config.allowsInlineMediaPlayback = true + config.mediaTypesRequiringUserActionForPlayback = [] + config.allowsPictureInPictureMediaPlayback = true + + let userController = WKUserContentController() + + var handleScriptMessage: ((WKScriptMessage) -> Void)? + userController.add(WeakScriptMessageHandler { message in + handleScriptMessage?(message) + }, name: "performAction") + + let isDebug: Bool + #if DEBUG + isDebug = true + #else + isDebug = false + #endif + + config.userContentController = userController + + let webView = WKWebView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)), configuration: config) + self.webView = webView + + webView.scrollView.isScrollEnabled = false + webView.allowsLinkPreview = false + webView.allowsBackForwardNavigationGestures = false + webView.accessibilityIgnoresInvertColors = true + webView.scrollView.contentInsetAdjustmentBehavior = .never + webView.alpha = 0.0 + + if #available(iOS 16.4, *) { + webView.isInspectable = isDebug + } + + webView.navigationDelegate = self + + handleScriptMessage = { [weak self] message in + Queue.mainQueue().async { + guard let self else { + return + } + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["event"] as? String else { + return + } + + switch eventName { + case "windowOnLoad": + self.isWebViewReady = true + + self.initializePendingInstances() + case "bridgeInvoke": + guard let eventData = body["data"] as? [String: Any] else { + return + } + guard let bridgeId = eventData["bridgeId"] as? Int else { + return + } + guard let callbackId = eventData["callbackId"] as? Int else { + return + } + guard let className = eventData["className"] as? String else { + return + } + guard let methodName = eventData["methodName"] as? String else { + return + } + guard let params = eventData["params"] as? [String: Any] else { + return + } + self.bridgeInvoke( + bridgeId: bridgeId, + className: className, + methodName: methodName, + params: params, + completion: { [weak self] result in + guard let self else { + return + } + let jsonResult = try! JSONSerialization.data(withJSONObject: result) + let jsonResultString = String(data: jsonResult, encoding: .utf8)! + self.webView?.evaluateJavaScript("bridgeInvokeCallback(\(callbackId), \(jsonResultString));", completionHandler: nil) + } + ) + case "playerStatus": + guard let instanceId = body["instanceId"] as? Int else { + return + } + guard let instance = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + return + } + guard let eventData = body["data"] as? [String: Any] else { + return + } + + instance.onPlayerStatusUpdated(eventData: eventData) + case "playerCurrentTime": + guard let instanceId = body["instanceId"] as? Int else { + return + } + guard let instance = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + return + } + guard let eventData = body["data"] as? [String: Any] else { + return + } + guard let value = eventData["value"] as? Double else { + return + } + + instance.onPlayerUpdatedCurrentTime(currentTime: value) + + var bandwidthEstimate = eventData["bandwidthEstimate"] as? Double + if let bandwidthEstimateValue = bandwidthEstimate, bandwidthEstimateValue.isNaN || bandwidthEstimateValue.isInfinite { + bandwidthEstimate = nil + } + + HLSVideoJSNativeContentNode.sharedBandwidthEstimate = bandwidthEstimate + default: + break + } + } + } + + self.isWebViewReady = false + + let bundle = Bundle(for: SharedHLSVideoWebView.self) + let bundlePath = bundle.bundlePath + "/HlsBundle.bundle" + webView.loadFileURL(URL(fileURLWithPath: bundlePath + "/index.html"), allowingReadAccessTo: URL(fileURLWithPath: bundlePath)) + } + + private func disposeWebView() { + if let _ = self.webView { + self.webView = nil + } + self.isWebViewReady = false + } + + private func bridgeInvoke( + bridgeId: Int, + className: String, + methodName: String, + params: [String: Any], + completion: @escaping ([String: Any]) -> Void + ) { + if (className == "VideoElement") { + if (methodName == "constructor") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + let videoElement = VideoElement(instanceId: instanceId) + SharedHLSVideoWebView.shared.videoElements[bridgeId] = videoElement + completion([:]) + } else if (methodName == "setMediaSource") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let mediaSourceId = params["mediaSourceId"] as? Int else { + assertionFailure() + return + } + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == instanceId }) else { + return + } + videoElement.mediaSourceId = mediaSourceId + } else if (methodName == "setCurrentTime") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let currentTime = params["currentTime"] as? Double else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onSetCurrentTime(timestamp: currentTime) + } + + completion([:]) + } else if (methodName == "setPlaybackRate") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let playbackRate = params["playbackRate"] as? Double else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onSetPlaybackRate(playbackRate: playbackRate) + } + + completion([:]) + } else if (methodName == "play") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onPlay() + } + + completion([:]) + } else if (methodName == "pause") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onPause() + } + + completion([:]) + } + } else if (className == "MediaSource") { + if (methodName == "constructor") { + let mediaSource = MediaSource() + SharedHLSVideoWebView.shared.mediaSources[bridgeId] = mediaSource + completion([:]) + } else if (methodName == "setDuration") { + guard let duration = params["duration"] as? Double else { + assertionFailure() + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else { + assertionFailure() + return + } + var durationUpdated = false + if mediaSource.duration != duration { + mediaSource.duration = duration + durationUpdated = true + } + + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else { + return + } + + if let instance = self.contextReferences[videoElement.instanceId]?.contentNode { + if durationUpdated { + instance.onMediaSourceDurationUpdated() + } + } + completion([:]) + } else if (methodName == "updateSourceBuffers") { + guard let ids = params["ids"] as? [Int] else { + assertionFailure() + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else { + assertionFailure() + return + } + mediaSource.sourceBufferIds = ids + + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else { + return + } + + if let instance = self.contextReferences[videoElement.instanceId]?.contentNode { + instance.onMediaSourceBuffersUpdated() + } + } + } else if (className == "SourceBuffer") { + if (methodName == "constructor") { + guard let mediaSourceId = params["mediaSourceId"] as? Int else { + assertionFailure() + return + } + guard let mimeType = params["mimeType"] as? String else { + assertionFailure() + return + } + let sourceBuffer = SourceBuffer(mediaSourceId: mediaSourceId, mimeType: mimeType) + SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] = sourceBuffer + + completion([:]) + } else if (methodName == "appendBuffer") { + guard let base64Data = params["data"] as? String else { + assertionFailure() + return + } + guard let data = Data(base64Encoded: base64Data.data(using: .utf8)!) else { + assertionFailure() + return + } + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.appendBuffer(data: data, completion: { bufferedRanges in + completion(["ranges": serializeRanges(bufferedRanges)]) + }) + } else if methodName == "remove" { + guard let start = params["start"] as? Double, let end = params["end"] as? Double else { + assertionFailure() + return + } + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.remove(start: start, end: end, completion: { bufferedRanges in + completion(["ranges": serializeRanges(bufferedRanges)]) + }) + } else if methodName == "abort" { + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.abortOperation() + completion([:]) + } + } else if className == "XMLHttpRequest" { + if methodName == "load" { + guard let id = params["id"] as? Int else { + assertionFailure() + return + } + guard let url = params["url"] as? String else { + assertionFailure() + return + } + guard let requestHeaders = params["requestHeaders"] as? [String: String] else { + assertionFailure() + return + } + guard let parsedUrl = URL(string: url) else { + assertionFailure() + return + } + guard let host = parsedUrl.host, host == "server" else { + completion(["error": 1]) + return + } + + var requestPath = parsedUrl.path + if requestPath.hasPrefix("/") { + requestPath = String(requestPath[requestPath.index(after: requestPath.startIndex) ..< requestPath.endIndex]) + } + + guard let firstSlash = requestPath.range(of: "/") else { + completion(["error": 1]) + return + } + + var requestRange: Range? + if let rangeString = requestHeaders["Range"] { + requestRange = parseRange(from: rangeString) + } + + let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound]) + + var handlerFound = false + for (_, contextReference) in self.contextReferences { + if let context = contextReference.contentNode, let source = context.playerSource, source.id == streamId { + handlerFound = true + + let filePath = String(requestPath[firstSlash.upperBound...]) + if filePath == "master.m3u8" { + let _ = (source.masterPlaylistData() + |> deliverOn(.mainQueue()) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion) + }) + } else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") { + guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else { + self.sendErrorAndClose(id: id, error: .notFound, completion: completion) + return + } + + let _ = (source.playlistData(quality: levelIndex) + |> deliverOn(.mainQueue()) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion) + }) + } else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)]) + guard let fileIdValue = Int64(fileId) else { + self.sendErrorAndClose(id: id, error: .notFound, completion: completion) + return + } + guard let requestRange else { + self.sendErrorAndClose(id: id, error: .badRequest, completion: completion) + return + } + let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) + |> deliverOn(.mainQueue()) + //|> timeout(5.0, queue: self.queue, alternate: .single(nil)) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let (tempFile, tempFileRange, totalSize) = result { + self.sendResponseFileAndClose(id: id, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize, completion: completion) + } else { + self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion) + } + }) + } + + break + } + } + + if (!handlerFound) { + completion(["error": 1]) + } + + /*var request = URLRequest(url: URL(string: url)!) + for (key, value) in requestHeaders { + request.setValue(value, forHTTPHeaderField: key) + } + + let isCompleted = Atomic(value: false) + let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] data, response, error in + Queue.mainQueue().async { + guard let self else { + return + } + if isCompleted.swap(true) { + return + } + + self.tempTasks.removeValue(forKey: id) + + if let _ = error { + completion([ + "error": 1 + ]) + } else { + if let response = response as? HTTPURLResponse { + completion([ + "status": response.statusCode, + "statusText": "OK", + "responseData": data?.base64EncodedString() ?? "", + "responseHeaders": response.allHeaderFields as? [String: String] ?? [:] + ]) + + let _ = response + /*if let response = response as? HTTPURLResponse, let requestUrl { + if let updatedResponse = HTTPURLResponse( + url: requestUrl, + statusCode: response.statusCode, + httpVersion: "HTTP/1.1", + headerFields: response.allHeaderFields as? [String: String] ?? [:] + ) { + sourceTask.didReceive(updatedResponse) + } else { + sourceTask.didReceive(response) + } + } else { + sourceTask.didReceive(response) + }*/ + } + } + } + }) + self.tempTasks[id] = task + task.resume()*/ + } else if methodName == "abort" { + guard let id = params["id"] as? Int else { + assertionFailure() + return + } + + if let task = self.tempTasks.removeValue(forKey: id) { + task.cancel() + } + + completion([:]) + } + } + } + + private func sendErrorAndClose(id: Int, error: ResponseError, completion: @escaping ([String: Any]) -> Void) { + let (code, status) = error.httpStatus + completion([ + "status": code, + "statusText": status, + "responseData": "", + "responseHeaders": [ + "Content-Type": "text/html" + ] as [String: String] + ]) + } + + private func sendResponseAndClose(id: Int, data: Data, contentType: String = "application/octet-stream", completion: @escaping ([String: Any]) -> Void) { + completion([ + "status": 200, + "statusText": "OK", + "responseData": data.base64EncodedString(), + "responseHeaders": [ + "Content-Type": contentType, + "Content-Length": "\(data.count)" + ] as [String: String] + ]) + } + + private func sendResponseFileAndClose(id: Int, file: TempBoxFile, fileRange: Range, range: Range, totalSize: Int, completion: @escaping ([String: Any]) -> Void) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe).subdata(in: fileRange) { + completion([ + "status": 200, + "statusText": "OK", + "responseData": data.base64EncodedString(), + "responseHeaders": [ + "Content-Type": "application/octet-stream", + "Content-Range": "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)", + "Content-Length": "\(fileRange.upperBound - fileRange.lowerBound)" + ] as [String: String] + ]) + } else { + self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion) + } + } + + func register(context: HLSVideoJSNativeContentNode) -> Disposable { + let contextInstanceId = context.instanceId + self.contextReferences[contextInstanceId] = ContextReference(contentNode: context) + + if self.webView == nil { + self.createWebView() + } + + if let emptyTimer = self.emptyTimer { + self.emptyTimer = nil + emptyTimer.invalidate() + } + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let self else { + return + } + self.pendingInitializeInstanceIds.removeAll(where: { $0.id == contextInstanceId }) + + if let current = self.contextReferences[contextInstanceId] { + if let value = current.contentNode { + if let context, context === value { + self.contextReferences.removeValue(forKey: contextInstanceId) + } + } else { + self.contextReferences.removeValue(forKey: contextInstanceId) + } + } + + self.webView?.evaluateJavaScript("window.hlsPlayer_destroyInstance(\(contextInstanceId));") + + if self.contextReferences.isEmpty { + if self.emptyTimer == nil { + self.emptyTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { [weak self] timer in + guard let self else { + return + } + if self.emptyTimer === timer { + self.emptyTimer = nil + } + if self.contextReferences.isEmpty { + self.disposeWebView() + } + }) + } + } + } + } + } + + func initializeWhenReady(context: HLSVideoJSNativeContentNode, urlPrefix: String) { + self.pendingInitializeInstanceIds.append((context.instanceId, urlPrefix)) + + if self.isWebViewReady { + self.initializePendingInstances() + } + } + + private func initializePendingInstances() { + let pendingInitializeInstanceIds = self.pendingInitializeInstanceIds + self.pendingInitializeInstanceIds.removeAll() + + if pendingInitializeInstanceIds.isEmpty { + return + } + + let isDebug: Bool + #if DEBUG + isDebug = true + #else + isDebug = false + #endif + + var userScriptJs = "" + for (instanceId, urlPrefix) in pendingInitializeInstanceIds { + guard let _ = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + continue + } + userScriptJs.append("window.hlsPlayer_makeInstance(\(instanceId));\n") + userScriptJs.append(""" + window.hlsPlayer_instances[\(instanceId)].playerInitialize({ + 'debug': \(isDebug), + 'bandwidthEstimate': \(HLSVideoJSNativeContentNode.sharedBandwidthEstimate ?? 500000.0), + 'urlPrefix': '\(urlPrefix)' + });\n + """) + } + + self.webView?.evaluateJavaScript(userScriptJs) + } +} + +final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNode { + fileprivate struct Level { + let bitrate: Int + let width: Int + let height: Int + + init(bitrate: Int, width: Int, height: Int) { + self.bitrate = bitrate + self.width = width + self.height = height + } + } + + private struct VideoQualityState: Equatable { + var current: Int + var preferred: UniversalVideoContentVideoQuality + var available: [Int] + + init(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int]) { + self.current = current + self.preferred = preferred + self.available = available + } + } + + fileprivate static var sharedBandwidthEstimate: Double? + + private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let fileReference: FileMediaReference + private let approximateDuration: Double + private let intrinsicDimensions: CGSize + + private let audioSessionManager: ManagedAudioSession + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + fileprivate let playerSource: HLSJSServerSource? + private var serverDisposable: Disposable? + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + private var isBuffering = false + private var seekId: Int = 0 + private let _status = ValuePromise() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(RangeSet, Int64)?>() + var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { + return self._bufferingStatus.get() + } + + var isNativePictureInPictureActive: Signal { + return .single(false) + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private static var nextInstanceId: Int = 0 + fileprivate let instanceId: Int + + private let imageNode: TransformImageNode + + private let player: ChunkMediaPlayer + private let playerNode: MediaPlayerNode + + private let fetchDisposable = MetaDisposable() + + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: (size: CGSize, actualSize: CGSize)? + + private var statusTimer: Foundation.Timer? + + private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto + + fileprivate var playerIsReady: Bool = false + fileprivate var playerIsPlaying: Bool = false + fileprivate var playerRate: Double = 0.0 + fileprivate var playerDefaultRate: Double = 1.0 + fileprivate var playerTime: Double = 0.0 + + fileprivate var playerAvailableLevels: [Int: Level] = [:] + fileprivate var playerCurrentLevelIndex: Int? + + private var videoQualityStateValue: VideoQualityState? + private let videoQualityStatePromise = Promise(nil) + + private var hasRequestedPlayerLoad: Bool = false + + private var requestedBaseRate: Double = 1.0 + private var requestedLevelIndex: Int? + + private var didBecomeActiveObserver: NSObjectProtocol? + private var willResignActiveObserver: NSObjectProtocol? + + private let chunkPlayerPartsState = Promise(ChunkMediaPlayerPartsState(duration: nil, parts: [])) + private var sourceBufferStateDisposable: Disposable? + + private var playerStatusDisposable: Disposable? + + private var contextDisposable: Disposable? + + init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + self.instanceId = HLSVideoJSNativeContentNode.nextInstanceId + HLSVideoJSNativeContentNode.nextInstanceId += 1 + + self.postbox = postbox + self.fileReference = fileReference + self.approximateDuration = fileReference.media.duration ?? 0.0 + self.audioSessionManager = audioSessionManager + self.userLocation = userLocation + self.requestedBaseRate = baseRate + + if var dimensions = fileReference.media.dimensions { + if let thumbnail = fileReference.media.previewRepresentations.first { + let dimensionsVertical = dimensions.width < dimensions.height + let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height + if dimensionsVertical != thumbnailVertical { + dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width) + } + } + self.dimensions = dimensions.cgSize + } else { + self.dimensions = CGSize(width: 128.0, height: 128.0) + } + + self.imageNode = TransformImageNode() + + var playerSource: HLSJSServerSource? + if let qualitySet = HLSQualitySet(baseFile: fileReference) { + let playerSourceValue = HLSJSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) + playerSource = playerSourceValue + } + self.playerSource = playerSource + + let mediaDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) + var intrinsicDimensions = mediaDimensions.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) + + intrinsicDimensions.width = floor(intrinsicDimensions.width / UIScreenScale) + intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale) + self.intrinsicDimensions = intrinsicDimensions + + var onSeeked: (() -> Void)? + self.player = ChunkMediaPlayer( + postbox: postbox, + audioSessionManager: audioSessionManager, + partsState: self.chunkPlayerPartsState.get(), + video: true, + enableSound: true, + baseRate: baseRate, + onSeeked: { + onSeeked?() + } + ) + + self.playerNode = MediaPlayerNode() + self.player.attachPlayerNode(self.playerNode) + + super.init() + + self.contextDisposable = SharedHLSVideoWebView.shared.register(context: self) + + self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + + let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference)?.file ?? fileReference + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: thumbnailVideoReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) + } + } + } + } + return getData + }) + + self.addSubnode(self.imageNode) + self.addSubnode(self.playerNode) + + self.imageNode.imageUpdated = { [weak self] _ in + self?._ready.set(.single(Void())) + } + + self._bufferingStatus.set(.single(nil)) + + self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in + let _ = self + }) + self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in + let _ = self + }) + + self.playerStatusDisposable = (self.player.status + |> deliverOnMainQueue).startStrict(next: { [weak self] status in + guard let self else { + return + } + self.updatePlayerStatus(status: status) + }) + + self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 25.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateStatus() + }) + + onSeeked = { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerNotifySeekedOnNextStatusUpdate();", completionHandler: nil) + } + } + + if let playerSource { + SharedHLSVideoWebView.shared.initializeWhenReady(context: self, urlPrefix: "http://server/\(playerSource.id)/") + } + } + + deinit { + if let didBecomeActiveObserver = self.didBecomeActiveObserver { + NotificationCenter.default.removeObserver(didBecomeActiveObserver) + } + if let willResignActiveObserver = self.willResignActiveObserver { + NotificationCenter.default.removeObserver(willResignActiveObserver) + } + + self.serverDisposable?.dispose() + self.audioSessionDisposable.dispose() + + self.statusTimer?.invalidate() + + self.sourceBufferStateDisposable?.dispose() + self.playerStatusDisposable?.dispose() + + self.contextDisposable?.dispose() + } + + fileprivate func onPlayerStatusUpdated(eventData: [String: Any]) { + if let isReady = eventData["isReady"] as? Bool { + self.playerIsReady = isReady + } else { + self.playerIsReady = false + } + if let isPlaying = eventData["isPlaying"] as? Bool { + self.playerIsPlaying = isPlaying + } else { + self.playerIsPlaying = false + } + if let rate = eventData["rate"] as? Double { + self.playerRate = rate + } else { + self.playerRate = 0.0 + } + if let defaultRate = eventData["defaultRate"] as? Double { + self.playerDefaultRate = defaultRate + } else { + self.playerDefaultRate = 0.0 + } + if let levels = eventData["levels"] as? [[String: Any]] { + self.playerAvailableLevels.removeAll() + + for level in levels { + guard let levelIndex = level["index"] as? Int else { + continue + } + guard let levelBitrate = level["bitrate"] as? Int else { + continue + } + guard let levelWidth = level["width"] as? Int else { + continue + } + guard let levelHeight = level["height"] as? Int else { + continue + } + self.playerAvailableLevels[levelIndex] = HLSVideoJSNativeContentNode.Level( + bitrate: levelBitrate, + width: levelWidth, + height: levelHeight + ) + } + } else { + self.playerAvailableLevels.removeAll() + } + + if let currentLevel = eventData["currentLevel"] as? Int { + if self.playerAvailableLevels[currentLevel] != nil { + self.playerCurrentLevelIndex = currentLevel + } else { + self.playerCurrentLevelIndex = nil + } + } else { + self.playerCurrentLevelIndex = nil + } + + self.updateVideoQualityState() + + if self.playerIsReady { + if !self.hasRequestedPlayerLoad { + if !self.playerAvailableLevels.isEmpty { + var selectedLevelIndex: Int? + if let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file { + if let dimensions = minimizedQualityFile.media.dimensions { + for (index, level) in self.playerAvailableLevels { + if level.height == Int(dimensions.height) { + selectedLevelIndex = index + break + } + } + } + } + if selectedLevelIndex == nil { + selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key + } + if let selectedLevelIndex { + self.hasRequestedPlayerLoad = true + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerLoad(\(selectedLevelIndex));", completionHandler: nil) + } + } + } + + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil) + } + + self.updateStatus() + } + + fileprivate func onPlayerUpdatedCurrentTime(currentTime: Double) { + self.playerTime = currentTime + + self.updateStatus() + } + + fileprivate func onSetCurrentTime(timestamp: Double) { + self.player.seek(timestamp: timestamp) + } + + fileprivate func onSetPlaybackRate(playbackRate: Double) { + self.player.setBaseRate(playbackRate) + } + + fileprivate func onPlay() { + self.player.play() + } + + fileprivate func onPause() { + self.player.pause() + } + + fileprivate func onMediaSourceDurationUpdated() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + } + + fileprivate func onMediaSourceBuffersUpdated() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + if self.sourceBufferStateDisposable == nil { + self.sourceBufferStateDisposable = (sourceBuffer.updated.signal() + |> deliverOnMainQueue).startStrict(next: { [weak self, weak sourceBuffer] _ in + guard let self, let sourceBuffer else { + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[sourceBuffer.mediaSourceId] else { + return + } + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + + self.updateBuffered() + }) + } + } + + private func updatePlayerStatus(status: MediaPlayerStatus) { + self._status.set(status) + + if let (bridgeId, _) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) { + var isPlaying: Bool = false + var isBuffering = false + switch status.status { + case .playing: + isPlaying = true + case .paused: + break + case let .buffering(_, whilePlaying, _, _): + isPlaying = whilePlaying + isBuffering = true + } + + let result: [String: Any] = [ + "isPlaying": isPlaying, + "isWaiting": isBuffering, + "currentTime": status.timestamp + ] + + let jsonResult = try! JSONSerialization.data(withJSONObject: result) + let jsonResultString = String(data: jsonResult, encoding: .utf8)! + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.bridgeObjectMap[\(bridgeId)].bridgeUpdateStatus(\(jsonResultString));", completionHandler: nil) + } + } + + private func updateBuffered() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + let bufferedRanges = sourceBuffer.ranges + + if let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) { + if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] { + if let duration = mediaSource.duration { + var mappedRanges = RangeSet() + for range in bufferedRanges.ranges { + let rangeLower = max(0.0, range.lowerBound - 0.2) + let rangeUpper = min(duration, range.upperBound + 0.2) + mappedRanges.formUnion(RangeSet(Int64(rangeLower * 1000.0) ..< Int64(rangeUpper * 1000.0))) + } + self._bufferingStatus.set(.single((mappedRanges, Int64(duration * 1000.0)))) + } + } + } + } + + private func updateStatus() { + } + + private func performActionAtEnd() { + for listener in self.playbackCompletedListeners.copyItems() { + listener() + } + } + + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + + if let dimensions = self.dimensions { + let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + } + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) + } + self.player.play() + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.player.pause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + self.player.togglePlayPause() + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + if value { + self.player.playOnceWithSound(playAndRecord: false, seek: .none) + } else { + self.player.continuePlayingWithoutSound(seek: .none) + } + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 + + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSeek(\(timestamp));", completionHandler: nil) + } + + func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + assert(Queue.mainQueue().isCurrent()) + let action = { [weak self] in + Queue.mainQueue().async { + self?.performActionAtEnd() + } + } + switch actionAtEnd { + case .loop: + self.player.actionAtEnd = .loop({}) + case .loopDisablingSound: + self.player.actionAtEnd = .loopDisablingSound(action) + case .stop: + self.player.actionAtEnd = .action(action) + case .repeatIfNeeded: + let _ = (self.player.status + |> deliverOnMainQueue + |> take(1)).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + if status.timestamp > status.duration * 0.1 { + strongSelf.player.actionAtEnd = .loop({ [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.player.actionAtEnd = .loopDisablingSound(action) + }) + } else { + strongSelf.player.actionAtEnd = .loopDisablingSound(action) + } + }) + } + + self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) + } + + func setSoundMuted(soundMuted: Bool) { + self.player.setSoundMuted(soundMuted: soundMuted) + } + + func continueWithOverridingAmbientMode(isAmbient: Bool) { + self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient) + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + assert(Queue.mainQueue().isCurrent()) + self.player.setForceAudioToSpeaker(forceAudioToSpeaker) + } + + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + assert(Queue.mainQueue().isCurrent()) + let action = { [weak self] in + Queue.mainQueue().async { + self?.performActionAtEnd() + } + } + switch actionAtEnd { + case .loop: + self.player.actionAtEnd = .loop({}) + case .loopDisablingSound, .repeatIfNeeded: + self.player.actionAtEnd = .loopDisablingSound(action) + case .stop: + self.player.actionAtEnd = .action(action) + } + self.player.continuePlayingWithoutSound(seek: .none) + } + + func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + self.player.setContinuePlayingWithoutSoundOnLostAudioSession(value) + } + + func setBaseRate(_ baseRate: Double) { + self.requestedBaseRate = baseRate + if self.playerIsReady { + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil) + } + self.updateStatus() + } + + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.preferredVideoQuality = videoQuality + + switch videoQuality { + case .auto: + self.requestedLevelIndex = nil + case let .quality(quality): + if let level = self.playerAvailableLevels.first(where: { min($0.value.width, $0.value.height) == quality }) { + self.requestedLevelIndex = level.key + } else { + self.requestedLevelIndex = nil + } + } + + self.updateVideoQualityState() + + if self.playerIsReady { + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetLevel(\(self.requestedLevelIndex ?? -1));", completionHandler: nil) + } + } + + private func updateVideoQualityState() { + var videoQualityState: VideoQualityState? + if let value = self.videoQualityState() { + videoQualityState = VideoQualityState(current: value.current, preferred: value.preferred, available: value.available) + } + if self.videoQualityStateValue != videoQualityState { + self.videoQualityStateValue = videoQualityState + self.videoQualityStatePromise.set(.single(videoQualityState)) + } + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + if self.playerAvailableLevels.isEmpty { + if let qualitySet = HLSQualitySet(baseFile: self.fileReference), let minQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file { + let sortedFiles = qualitySet.qualityFiles.sorted(by: { $0.key > $1.key }) + if let minQuality = sortedFiles.first(where: { $0.value.media.fileId == minQualityFile.media.fileId }) { + return (minQuality.key, .auto, sortedFiles.map(\.key)) + } + } + } + + guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else { + return nil + } + guard let currentLevel = self.playerAvailableLevels[playerCurrentLevelIndex] else { + return nil + } + + var available = self.playerAvailableLevels.values.map { min($0.width, $0.height) } + available.sort(by: { $0 > $1 }) + + return (min(currentLevel.width, currentLevel.height), self.preferredVideoQuality, available) + } + + public func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return self.videoQualityStatePromise.get() + |> map { value -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? in + guard let value else { + return nil + } + return (value.current, value.preferred, value.available) + } + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } + + func notifyPlaybackControlsHidden(_ hidden: Bool) { + } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy) + } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + self.imageNode.isHidden = !value + } +} + +private func serializeRanges(_ ranges: RangeSet) -> [Double] { + var result: [Double] = [] + for range in ranges.ranges { + result.append(range.lowerBound) + result.append(range.upperBound) + } + return result +} + +private final class VideoElement { + let instanceId: Int + + var mediaSourceId: Int? + + init(instanceId: Int) { + self.instanceId = instanceId + } +} + +private final class MediaSource { + var duration: Double? + var sourceBufferIds: [Int] = [] + + init() { + } +} + +private final class SourceBuffer { + private static let sharedQueue = Queue(name: "SourceBuffer") + + final class Item { + let tempFile: TempBoxFile + let asset: AVURLAsset + let startTime: Double + let endTime: Double + let rawData: Data + + var clippedStartTime: Double + var clippedEndTime: Double + + init(tempFile: TempBoxFile, asset: AVURLAsset, startTime: Double, endTime: Double, rawData: Data) { + self.tempFile = tempFile + self.asset = asset + self.startTime = startTime + self.endTime = endTime + self.rawData = rawData + + self.clippedStartTime = startTime + self.clippedEndTime = endTime + } + + func removeRange(start: Double, end: Double) { + //TODO + } + } + + let mediaSourceId: Int + let mimeType: String + var initializationData: Data? + var items: [ChunkMediaPlayerPart] = [] + var ranges = RangeSet() + + let updated = ValuePipe() + + private var currentUpdateId: Int = 0 + + init(mediaSourceId: Int, mimeType: String) { + self.mediaSourceId = mediaSourceId + self.mimeType = mimeType + } + + func abortOperation() { + self.currentUpdateId += 1 + } + + func appendBuffer(data: Data, completion: @escaping (RangeSet) -> Void) { + let initializationData = self.initializationData + self.currentUpdateId += 1 + let updateId = self.currentUpdateId + + SourceBuffer.sharedQueue.async { [weak self] in + let tempFile = TempBox.shared.tempFile(fileName: "data.mp4") + + var combinedData = Data() + if let initializationData { + combinedData.append(initializationData) + } + combinedData.append(data) + guard let _ = try? combinedData.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) else { + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + completion(self.ranges) + } + return + } + + if let fragmentInfo = extractFFMpegMediaInfo(path: tempFile.path) { + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + if fragmentInfo.duration.value == 0 { + self.initializationData = data + + completion(self.ranges) + } else { + let item = ChunkMediaPlayerPart( + startTime: fragmentInfo.startTime.seconds, + endTime: fragmentInfo.startTime.seconds + fragmentInfo.duration.seconds, + file: tempFile + ) + self.items.append(item) + self.updateRanges() + + completion(self.ranges) + + self.updated.putNext(Void()) + } + } + } else { + assertionFailure() + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + completion(self.ranges) + } + return + } + } + } + + func remove(start: Double, end: Double, completion: @escaping (RangeSet) -> Void) { + self.items.removeAll(where: { item in + if item.startTime >= start && item.endTime <= end { + return true + } else { + return false + } + }) + self.updateRanges() + completion(self.ranges) + + self.updated.putNext(Void()) + } + + private func updateRanges() { + self.ranges = RangeSet() + for item in self.items { + let itemStartTime = round(item.startTime * 1000.0) / 1000.0 + let itemEndTime = round(item.endTime * 1000.0) / 1000.0 + self.ranges.formUnion(RangeSet(itemStartTime ..< itemEndTime)) + } + } +} + +private func parseFragment(filePath: String) -> (offset: CMTime, duration: CMTime)? { + let source = SoftwareVideoSource(path: filePath, hintVP9: false, unpremultiplyAlpha: false) + return source.readTrackInfo() +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 3df84b67294..7a157b4635f 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -11,6 +11,7 @@ import AccountContext import PhotoResources import UIKitRuntimeUtils import RangeSet +import VideoToolbox private extension CGRect { var center: CGPoint { @@ -25,11 +26,18 @@ public enum NativeVideoContentId: Hashable { case profileVideo(Int64, String?) } +private let isAv1Supported: Bool = { + let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) + return value +}() + public final class NativeVideoContent: UniversalVideoContent { public let id: AnyHashable public let nativeId: NativeVideoContentId public let userLocation: MediaResourceUserLocation public let fileReference: FileMediaReference + public let previewSourceFileReference: FileMediaReference? + public let limitedFileRange: Range? let imageReference: ImageMediaReference? public let dimensions: CGSize public let duration: Double @@ -40,6 +48,7 @@ public final class NativeVideoContent: UniversalVideoContent { public let beginWithAmbientSound: Bool public let mixWithOthers: Bool public let baseRate: Double + public let baseVideoQuality: UniversalVideoContentVideoQuality let fetchAutomatically: Bool let onlyFullSizeThumbnail: Bool let useLargeThumbnail: Bool @@ -56,11 +65,58 @@ public final class NativeVideoContent: UniversalVideoContent { let displayImage: Bool let hasSentFramesToDisplay: (() -> Void)? - public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { + public static func isVideoCodecSupported(videoCodec: String) -> Bool { + if videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" { + return true + } + + /*if videoCodec == "av1" { + if isAv1Supported { + return true + } + }*/ + + return false + } + + public static func isHLSVideo(file: TelegramMediaFile) -> Bool { + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + if alternativeFile.mimeType == "application/x-mpegurl" { + return true + } + } + } + return false + } + + public static func selectVideoQualityFile(file: TelegramMediaFile, quality: UniversalVideoContentVideoQuality) -> TelegramMediaFile { + guard case let .quality(qualityHeight) = quality else { + return file + } + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + for attribute in alternativeFile.attributes { + if case let .Video(_, size, _, _, _, videoCodec) = attribute { + if let videoCodec, isVideoCodecSupported(videoCodec: videoCodec) { + if size.height == qualityHeight { + return alternativeFile + } + } + } + } + } + } + return file + } + + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, limitedFileRange: Range? = nil, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { self.id = id self.nativeId = id self.userLocation = userLocation self.fileReference = fileReference + self.previewSourceFileReference = previewSourceFileReference + self.limitedFileRange = limitedFileRange self.imageReference = imageReference if var dimensions = fileReference.media.dimensions { if let thumbnail = fileReference.media.previewRepresentations.first { @@ -83,6 +139,7 @@ public final class NativeVideoContent: UniversalVideoContent { self.beginWithAmbientSound = beginWithAmbientSound self.mixWithOthers = mixWithOthers self.baseRate = baseRate + self.baseVideoQuality = baseVideoQuality self.fetchAutomatically = fetchAutomatically self.onlyFullSizeThumbnail = onlyFullSizeThumbnail self.useLargeThumbnail = useLargeThumbnail @@ -100,8 +157,8 @@ public final class NativeVideoContent: UniversalVideoContent { self.hasSentFramesToDisplay = hasSentFramesToDisplay } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, previewSourceFileReference: self.previewSourceFileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -122,18 +179,23 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let postbox: Postbox private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference + private let previewSourceFileReference: FileMediaReference? + private let limitedFileRange: Range? + private let streamVideo: MediaPlayerStreaming private let enableSound: Bool private let soundMuted: Bool private let beginWithAmbientSound: Bool private let mixWithOthers: Bool private let loopVideo: Bool private let baseRate: Double + private var baseVideoQuality: UniversalVideoContentVideoQuality private let audioSessionManager: ManagedAudioSession private let isAudioVideoMessage: Bool private let captureProtected: Bool + private let continuePlayingWithoutSoundOnLostAudioSession: Bool private let displayImage: Bool - private let player: MediaPlayer + private var player: MediaPlayer private var thumbnailPlayer: MediaPlayer? private let imageNode: TransformImageNode private let playerNode: MediaPlayerNode @@ -166,6 +228,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -177,16 +243,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var dimensions: CGSize? private let dimensionsPromise = ValuePromise(CGSize()) - private var validLayout: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? private var shouldPlay: Bool = false private let hasSentFramesToDisplay: (() -> Void)? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, limitedFileRange: Range?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference + self.previewSourceFileReference = previewSourceFileReference + self.limitedFileRange = limitedFileRange + self.streamVideo = streamVideo self.placeholderColor = placeholderColor self.enableSound = enableSound self.soundMuted = soundMuted @@ -194,9 +263,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.mixWithOthers = mixWithOthers self.loopVideo = loopVideo self.baseRate = baseRate + self.baseVideoQuality = baseVideoQuality self.audioSessionManager = audioSessionManager self.isAudioVideoMessage = isAudioVideoMessage self.captureProtected = captureProtected + self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession self.displayImage = displayImage self.hasSentFramesToDisplay = hasSentFramesToDisplay @@ -210,7 +281,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent break } - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + let selectedFile = NativeVideoContent.selectVideoQualityFile(file: fileReference.media, quality: self.baseVideoQuality) + + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(selectedFile.resource), tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -255,14 +328,14 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent setLayerDisableScreenshots(self.imageNode.layer, captureProtected) } - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, previewSourceFileReference: previewSourceFileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { strongSelf.dimensions = dimensions strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -279,7 +352,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) }) - self.fetchStatusDisposable.set((postbox.mediaBox.resourceStatus(fileReference.media.resource) + self.fetchStatusDisposable.set((postbox.mediaBox.resourceStatus(selectedFile.resource) |> deliverOnMainQueue).start(next: { [weak self] status in guard let strongSelf = self else { return @@ -294,8 +367,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } })) - if let size = fileReference.media.size { - self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) |> map { ranges in + if let size = selectedFile.size { + self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(selectedFile.resource) |> map { ranges in return (ranges, size) }) } else { @@ -387,8 +460,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) if let dimensions = self.dimensions { let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) @@ -503,6 +576,101 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.setBaseRate(baseRate) } + func setVideoQuality(_ quality: UniversalVideoContentVideoQuality) { + let _ = (self._status.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + guard let self else { + return + } + + if self.baseVideoQuality == quality { + return + } + self.baseVideoQuality = quality + + let selectedFile = NativeVideoContent.selectVideoQualityFile(file: self.fileReference.media, quality: self.baseVideoQuality) + + let updatedFileReference: FileMediaReference = self.fileReference.withMedia(selectedFile) + + var userContentType = MediaResourceUserContentType(file: selectedFile) + switch updatedFileReference { + case .story: + userContentType = .story + default: + break + } + + self._status.set(.never()) + self.player.pause() + + self.player = MediaPlayer(audioSessionManager: self.audioSessionManager, postbox: self.postbox, userLocation: self.userLocation, userContentType: userContentType, resourceReference: updatedFileReference.resourceReference(selectedFile.resource), tempFilePath: nil, streamable: self.streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: true, soundMuted: self.soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: nil, isAudioVideoMessage: self.isAudioVideoMessage) + + var actionAtEndImpl: (() -> Void)? + if self.enableSound && !self.loopVideo { + self.player.actionAtEnd = .action({ + actionAtEndImpl?() + }) + } else { + self.player.actionAtEnd = .loop({ + actionAtEndImpl?() + }) + } + actionAtEndImpl = { [weak self] in + self?.performActionAtEnd() + } + + self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status) + |> map { dimensions, status in + return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) + }) + + self.fetchStatusDisposable.set((self.postbox.mediaBox.resourceStatus(selectedFile.resource) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + switch status { + case .Local: + break + default: + if strongSelf.thumbnailPlayer == nil { + strongSelf.createThumbnailPlayer() + } + } + })) + + if let size = updatedFileReference.media.size { + self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(selectedFile.resource) |> map { ranges in + return (ranges, size) + }) + } else { + self._bufferingStatus.set(.single(nil)) + } + + self.player.attachPlayerNode(self.playerNode) + + var play = false + switch status.status { + case .playing: + play = true + case let .buffering(_, whilePlaying, _, _): + play = whilePlaying + case .paused: + break + } + self.player.seek(timestamp: status.timestamp, play: play) + }) + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) let action = { [weak self] in @@ -548,4 +716,15 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy) } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + self.imageNode.isHidden = value + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index 1d8cc7c36aa..d71b1eec998 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -41,7 +41,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP private var statusDisposable: Disposable? private var status: MediaPlayerStatus? - public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { + public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content self.defaultExpand = expand @@ -62,7 +62,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP }, controlsAreShowingUpdated: { value in controlsAreShowingUpdatedImpl?(value) }) - self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) + self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration super.init() diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift index 0f5b97e1b9d..975b5344768 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift @@ -47,7 +47,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { private let statusDisposable = MetaDisposable() - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(contentDimensions: CGSize, unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void, controlsAreShowingUpdated: @escaping (Bool) -> Void) { self.contentDimensions = contentDimensions @@ -106,9 +106,9 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = self.frameForContent(size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = self.frameForContent(size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -118,8 +118,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { func updateContentNodeSnapshot(_ snapshot: UIView?) { } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let contentFrame = self.frameForContent(size: size) @@ -146,7 +146,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: contentFrame) - contentNode.updateLayout(size: contentFrame.size, transition: transition) + contentNode.updateLayout(size: contentFrame.size, actualSize: contentFrame.size, transition: transition) } } @@ -209,8 +209,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if self.minimizedBlurView == nil { let minimizedBlurView = UIVisualEffectView(effect: nil) self.minimizedBlurView = minimizedBlurView - if let validLayoutSize = self.validLayoutSize { - minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + if let validLayout = self.validLayout { + minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayout.size) } minimizedBlurView.isHidden = true self.foregroundContainerNode.view.addSubview(minimizedBlurView) @@ -222,8 +222,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { self.minimizedBlurView?.contentView.addSubview(minimizedArrowView) } if let minimizedArrowView = self.minimizedArrowView { - if let validLayoutSize = self.validLayoutSize { - setupArrowFrame(size: validLayoutSize, edge: edge, view: minimizedArrowView) + if let validLayout = self.validLayout { + setupArrowFrame(size: validLayout.size, edge: edge, view: minimizedArrowView) } minimizedArrowView.setAngled(!adjusting, animated: true) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 30bac1bc9cb..2481eb2a85a 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -70,7 +70,7 @@ public final class PlatformVideoContent: UniversalVideoContent { } public let id: AnyHashable - let nativeId: PlatformVideoContentId + public let nativeId: PlatformVideoContentId let userLocation: MediaResourceUserLocation let content: Content public let dimensions: CGSize @@ -95,7 +95,7 @@ public final class PlatformVideoContent: UniversalVideoContent { self.fetchAutomatically = fetchAutomatically } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } @@ -141,6 +141,10 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -170,7 +174,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private var dimensions: CGSize? private let dimensionsPromise = ValuePromise(CGSize()) - private var validLayout: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { self.postbox = postbox @@ -203,8 +207,8 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte if let dimensions = getSize() { strongSelf.dimensions = dimensions strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -371,7 +375,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -448,6 +452,17 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func setBaseRate(_ baseRate: Double) { } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -464,4 +479,14 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 756fcf51e96..fae099ae4c6 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -29,7 +29,7 @@ public final class SystemVideoContent: UniversalVideoContent { self.duration = duration } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } @@ -58,6 +58,10 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -207,7 +211,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -285,6 +289,17 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func setBaseRate(_ baseRate: Double) { } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -301,5 +316,15 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift b/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift index 84bef2160ad..0538d0d4a8d 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift @@ -31,9 +31,12 @@ private final class UniversalVideoContentHolder { var bufferingStatusDisposable: Disposable? var bufferingStatusValue: (RangeSet, Int64)? + var isNativePictureInPictureActiveDisposable: Disposable? + var isNativePictureInPictureActiveValue: Bool = false + var playbackCompletedIndex: Int? - init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet, Int64)?) -> Void, playbackCompleted: @escaping () -> Void) { + init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet, Int64)?) -> Void, playbackCompleted: @escaping () -> Void, isNativePictureInPictureActiveUpdated: @escaping (Bool) -> Void) { self.content = content self.contentNode = contentNode @@ -51,6 +54,13 @@ private final class UniversalVideoContentHolder { } }) + self.isNativePictureInPictureActiveDisposable = (contentNode.isNativePictureInPictureActive |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.isNativePictureInPictureActiveValue = value + isNativePictureInPictureActiveUpdated(value) + } + }) + self.playbackCompletedIndex = contentNode.addPlaybackCompleted { playbackCompleted() } @@ -59,6 +69,7 @@ private final class UniversalVideoContentHolder { deinit { self.statusDisposable?.dispose() self.bufferingStatusDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() if let playbackCompletedIndex = self.playbackCompletedIndex { self.contentNode.removePlaybackCompleted(playbackCompletedIndex) } @@ -133,9 +144,10 @@ private final class UniversalVideoContentHolderCallbacks { let playbackCompleted = Bag<() -> Void>() let status = Bag<(MediaPlayerStatus?) -> Void>() let bufferingStatus = Bag<((RangeSet, Int64)?) -> Void>() + let isNativePictureInPictureActive = Bag<(Bool) -> Void>() var isEmpty: Bool { - return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty + return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty && self.isNativePictureInPictureActive.isEmpty } } @@ -190,6 +202,14 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager { } } } + }, isNativePictureInPictureActiveUpdated: { [weak self] value in + if let strongSelf = self { + if let current = strongSelf.holderCallbacks[content.id] { + for subscriber in current.isNativePictureInPictureActive.copyItems() { + subscriber(value) + } + } + } }) self.holders[content.id] = holder } @@ -311,4 +331,37 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager { } } |> runOn(Queue.mainQueue()) } + + public func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal { + return Signal { subscriber in + var callbacks: UniversalVideoContentHolderCallbacks + if let current = self.holderCallbacks[content.id] { + callbacks = current + } else { + callbacks = UniversalVideoContentHolderCallbacks() + self.holderCallbacks[content.id] = callbacks + } + + let index = callbacks.isNativePictureInPictureActive.add({ value in + subscriber.putNext(value) + }) + + if let current = self.holders[content.id] { + subscriber.putNext(current.isNativePictureInPictureActiveValue) + } else { + subscriber.putNext(false) + } + + return ActionDisposable { + Queue.mainQueue().async { + if let current = self.holderCallbacks[content.id] { + current.status.remove(index) + if current.playbackCompleted.isEmpty { + self.holderCallbacks.removeValue(forKey: content.id) + } + } + } + } + } |> runOn(Queue.mainQueue()) + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index a680f3b7a7a..781ebfeb9c6 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import SwiftSignalKit -import WebKit +@preconcurrency import WebKit import TelegramCore import UniversalMediaPlayer @@ -227,4 +227,14 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate { func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index d45c70364b5..ebe1c8b9ac1 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -36,7 +36,7 @@ public final class WebEmbedVideoContent: UniversalVideoContent { self.openUrl = openUrl } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl) } } @@ -58,6 +58,10 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private var seekId: Int = 0 private let _ready = Promise() @@ -116,7 +120,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { self.readyDisposable.dispose() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -183,6 +187,17 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { self.playerNode.setBaseRate(baseRate) } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -200,4 +215,14 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index 9de5d24e9ab..80807f10f65 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/TgVoip:TgVoip", "//submodules/TgVoipWebrtc:TgVoipWebrtc", "//submodules/FFMpegBinding", + "//submodules/ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index cfb96f82082..85623961d57 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -759,6 +759,20 @@ public final class OngoingCallContext { CallAudioTone(samples: tone.samples, sampleRate: tone.sampleRate, loopCount: tone.loopCount) }) } + + // MARK: Nicegram NCG-5828 call recording + + public func startNicegramRecording( + callback: @escaping ((String, Double, UInt) -> Void), + errorCallback: @escaping ((String) -> Void) + ) { + self.impl.startNicegramRecording(callback, errorCallback: errorCallback) + } + + public func stopNicegramRecording() { + self.impl.stopNicegramRecording() + } + // } public static func setupAudioSession() { diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 115ee7edb84..5a405bff0ca 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -5,7 +5,7 @@ import TelegramCore import Network import Postbox import FFMpegBinding - +import ManagedFile @available(iOS 12.0, macOS 14.0, *) public final class WrappedMediaStreamingContext { @@ -138,7 +138,7 @@ public final class WrappedMediaStreamingContext { } } @available(iOS 12.0, macOS 14.0, *) -public final class ExternalMediaStreamingContext { +public final class ExternalMediaStreamingContext: SharedHLSServerSource { private final class Impl { let queue: Queue @@ -274,21 +274,29 @@ public final class ExternalMediaStreamingContext { ) } } + + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + return .never() + } } private let queue = Queue() - let id: CallSessionInternalId + let internalId: CallSessionInternalId private let impl: QueueLocalObject private var hlsServerDisposable: Disposable? + public var id: String { + return self.internalId.uuidString + } + public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) { - self.id = id + self.internalId = id let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, rejoinNeeded: rejoinNeeded) }) - self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(streamingContext: self) + self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self, completion: {}) } deinit { @@ -322,9 +330,32 @@ public final class ExternalMediaStreamingContext { impl.partData(index: index, quality: quality).start(next: subscriber.putNext) } } + + public func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + return self.impl.signalWith { impl, subscriber in + impl.fileData(id: id, range: range).start(next: subscriber.putNext) + } + } + + public func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return .single(nil) + } +} + +public protocol SharedHLSServerSource: AnyObject { + var id: String { get } + + func masterPlaylistData() -> Signal + func playlistData(quality: Int) -> Signal + func partData(index: Int, quality: Int) -> Signal + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> } + @available(iOS 12.0, macOS 14.0, *) public final class SharedHLSServer { + public typealias Source = SharedHLSServerSource + public static let shared: SharedHLSServer = { return SharedHLSServer() }() @@ -346,11 +377,11 @@ public final class SharedHLSServer { } } - private final class ContextReference { - weak var streamingContext: ExternalMediaStreamingContext? + private final class SourceReference { + weak var source: SharedHLSServerSource? - init(streamingContext: ExternalMediaStreamingContext) { - self.streamingContext = streamingContext + init(source: SharedHLSServerSource) { + self.source = source } } @available(iOS 12.0, macOS 14.0, *) @@ -360,15 +391,67 @@ public final class SharedHLSServer { private let port: NWEndpoint.Port private var listener: NWListener? - private var contextReferences = Bag() + private var sourceReferences = Bag() + private var referenceCheckTimer: SwiftSignalKit.Timer? + private var shutdownTimer: SwiftSignalKit.Timer? init(queue: Queue, port: UInt16) { self.queue = queue self.port = NWEndpoint.Port(rawValue: port)! - self.start() } - func start() { + deinit { + self.referenceCheckTimer?.invalidate() + self.shutdownTimer?.invalidate() + } + + private func updateNeedsListener() { + var isEmpty = true + for item in self.sourceReferences.copyItems() { + if let _ = item.source { + isEmpty = false + break + } + } + + if isEmpty { + if self.listener != nil { + if self.shutdownTimer == nil { + self.shutdownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + guard let self else { + return + } + self.shutdownTimer = nil + self.stopListener() + }, queue: self.queue) + self.shutdownTimer?.start() + } + } + if let referenceCheckTimer = self.referenceCheckTimer { + self.referenceCheckTimer = nil + referenceCheckTimer.invalidate() + } + } else { + if let shutdownTimer = self.shutdownTimer { + self.shutdownTimer = nil + shutdownTimer.invalidate() + } + if self.listener == nil { + self.startListener() + } + if self.referenceCheckTimer == nil { + self.referenceCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.updateNeedsListener() + }, queue: self.queue) + self.referenceCheckTimer?.start() + } + } + } + + private func startListener() { let listener: NWListener do { listener = try NWListener(using: .tcp, on: self.port) @@ -385,8 +468,8 @@ public final class SharedHLSServer { self.handleConnection(connection: connection) } - listener.stateUpdateHandler = { [weak self] state in - guard let self else { + listener.stateUpdateHandler = { [weak self, weak listener] state in + guard let self, let listener else { return } switch state { @@ -394,9 +477,9 @@ public final class SharedHLSServer { Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)") case let .failed(error): Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)") - self.listener?.cancel() + listener.cancel() - self.listener?.start(queue: self.queue.queue) + listener.start(queue: self.queue.queue) default: break } @@ -405,9 +488,17 @@ public final class SharedHLSServer { listener.start(queue: self.queue.queue) } + private func stopListener() { + guard let listener = self.listener else { + return + } + self.listener = nil + listener.cancel() + } + private func handleConnection(connection: NWConnection) { connection.start(queue: self.queue.queue) - connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in + connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] data, _, isComplete, error in guard let self else { return } @@ -443,23 +534,34 @@ public final class SharedHLSServer { } let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)]) + var requestRange: Range? + if let rangeRange = requestString.range(of: "Range: bytes=") { + if let endRange = requestString.range(of: "\r\n", range: rangeRange.upperBound ..< requestString.endIndex) { + let rangeString = String(requestString[rangeRange.upperBound ..< endRange.lowerBound]) + if let dashRange = rangeString.range(of: "-") { + let lowerBoundString = String(rangeString[rangeString.startIndex ..< dashRange.lowerBound]) + let upperBoundString = String(rangeString[dashRange.upperBound ..< rangeString.endIndex]) + + if let lowerBound = Int(lowerBoundString), let upperBound = Int(upperBoundString) { + requestRange = lowerBound ..< upperBound + } + } + } + } guard let firstSlash = requestPath.range(of: "/") else { self.sendErrorAndClose(connection: connection, error: .notFound) return } - guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else { - self.sendErrorAndClose(connection: connection) - return - } - guard let streamingContext = self.contextReferences.copyItems().first(where: { $0.streamingContext?.id == streamId })?.streamingContext else { + let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound]) + guard let source = self.sourceReferences.copyItems().first(where: { $0.source?.id == streamId })?.source else { self.sendErrorAndClose(connection: connection) return } let filePath = String(requestPath[firstSlash.upperBound...]) if filePath == "master.m3u8" { - let _ = (streamingContext.masterPlaylistData() + let _ = (source.masterPlaylistData() |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -474,7 +576,7 @@ public final class SharedHLSServer { return } - let _ = (streamingContext.playlistData(quality: levelIndex) + let _ = (source.playlistData(quality: levelIndex) |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -497,7 +599,7 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection) return } - let _ = (streamingContext.partData(index: partIndex, quality: levelIndex) + let _ = (source.partData(index: partIndex, quality: levelIndex) |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -529,8 +631,44 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection, error: .notFound) } }) + } else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)]) + guard let fileIdValue = Int64(fileId) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let requestRange else { + self.sendErrorAndClose(connection: connection) + return + } + let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) + |> deliverOn(self.queue) + //|> timeout(5.0, queue: self.queue, alternate: .single(nil)) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let (tempFile, tempFileRange, totalSize) = result { + self.sendResponseFileAndClose(connection: connection, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize) + } else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + } + }) } else { - self.sendErrorAndClose(connection: connection, error: .notFound) + let _ = (source.arbitraryFileData(path: filePath) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let result { + self.sendResponseAndClose(connection: connection, data: result.data, contentType: result.contentType) + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + }) } } @@ -544,8 +682,17 @@ public final class SharedHLSServer { }) } - private func sendResponseAndClose(connection: NWConnection, data: Data) { - let responseHeaders = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n" + private func sendResponseAndClose(connection: NWConnection, data: Data, contentType: String = "application/octet-stream", range: Range? = nil, totalSize: Int? = nil) { + var responseHeaders = "HTTP/1.1 200 OK\r\n" + responseHeaders.append("Content-Length: \(data.count)\r\n") + if let range, let totalSize { + responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n") + } + + responseHeaders.append("Content-Type: \(contentType)\r\n") + responseHeaders.append("Connection: close\r\n") + responseHeaders.append("Access-Control-Allow-Origin: *\r\n") + responseHeaders.append("\r\n") var responseData = Data() responseData.append(responseHeaders.data(using: .utf8)!) responseData.append(data) @@ -557,16 +704,70 @@ public final class SharedHLSServer { }) } - func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + private static func sendRemainingFileRange(queue: Queue, connection: NWConnection, tempFile: TempBoxFile, managedFile: ManagedFile, remainingRange: Range, fileSize: Int) -> Void { + let blockSize = 256 * 1024 + + let clippedLowerBound = min(remainingRange.lowerBound, fileSize) + var clippedUpperBound = min(remainingRange.upperBound, fileSize) + clippedUpperBound = min(clippedUpperBound, clippedLowerBound + blockSize) + + if clippedUpperBound == clippedLowerBound { + TempBox.shared.dispose(tempFile) + connection.cancel() + } else { + let _ = managedFile.seek(position: Int64(clippedLowerBound)) + let data = managedFile.readData(count: Int(clippedUpperBound - clippedLowerBound)) + let nextRange = clippedUpperBound ..< remainingRange.upperBound + + connection.send(content: data, completion: .contentProcessed { error in + queue.async { + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + connection.cancel() + TempBox.shared.dispose(tempFile) + } else { + sendRemainingFileRange(queue: queue, connection: connection, tempFile: tempFile, managedFile: managedFile, remainingRange: nextRange, fileSize: fileSize) + } + } + }) + } + } + + private func sendResponseFileAndClose(connection: NWConnection, file: TempBoxFile, fileRange: Range, range: Range, totalSize: Int) { + let queue = self.queue + + guard let managedFile = ManagedFile(queue: nil, path: file.path, mode: .read), let fileSize = managedFile.getSize() else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + TempBox.shared.dispose(file) + return + } + + var responseHeaders = "HTTP/1.1 200 OK\r\n" + responseHeaders.append("Content-Length: \(fileRange.upperBound - fileRange.lowerBound)\r\n") + responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n") + responseHeaders.append("Content-Type: application/octet-stream\r\n") + responseHeaders.append("Connection: close\r\n") + responseHeaders.append("Access-Control-Allow-Origin: *\r\n") + responseHeaders.append("\r\n") + + connection.send(content: responseHeaders.data(using: .utf8)!, completion: .contentProcessed({ _ in })) + + Impl.sendRemainingFileRange(queue: queue, connection: connection, tempFile: file, managedFile: managedFile, remainingRange: fileRange, fileSize: Int(fileSize)) + } + + func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable { let queue = self.queue - let index = self.contextReferences.add(ContextReference(streamingContext: streamingContext)) + let index = self.sourceReferences.add(SourceReference(source: source)) + self.updateNeedsListener() + completion() return ActionDisposable { [weak self] in queue.async { guard let self else { return } - self.contextReferences.remove(index) + self.sourceReferences.remove(index) + self.updateNeedsListener() } } } @@ -584,11 +785,11 @@ public final class SharedHLSServer { }) } - fileprivate func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + public func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable { let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.registerPlayer(streamingContext: streamingContext)) + disposable.set(impl.registerPlayer(source: source, completion: completion)) } return disposable diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 45b3e874faa..86aa5d1b9ab 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -409,6 +409,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case nameColors([UInt32]) case stars(tinted: Bool) case ton + case animation(name: String) } public let interactivelySelectedFromPackId: ItemCollectionId? diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 093f02443c2..b2537ee6794 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -209,11 +209,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } } if !skipEntity { + var hashtagValue = hashtag + var peerNameValue: String? + if hashtagValue.contains("@") { + let components = hashtagValue.components(separatedBy: "@") + if components.count == 2, let firstComponent = components.first, let lastComponent = components.last, !firstComponent.isEmpty && !lastComponent.isEmpty { + hashtagValue = firstComponent + peerNameValue = lastComponent + } + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } - string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerNameValue, hashtag: hashtagValue), range: range) } case .BotCommand: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index be9f8d91a2a..a124ee5bca2 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -169,6 +169,7 @@ objc_library( "//submodules/ffmpeg:ffmpeg", "//third-party/rnnoise:rnnoise", "//third-party/libyuv:libyuv", + "//submodules/OpusBinding:OpusBinding", ] + (["//third-party/libx264:libx264"] if enable_x264 else []), sdk_frameworks = [ "Foundation", diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 14c2f0c97f8..6a4d1d9e8ae 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -31,6 +31,11 @@ - (void)setTone:(CallAudioTone * _Nullable)tone; +// MARK: Nicegram NCG-5828 call recording +-(void)StartNicegramRecording:(void(^_Nullable)(NSString* _Nonnull, double, NSUInteger))completion + errorCallback:(void (^_Nullable)(NSString* _Nonnull))errorCallback; +-(void)StopNicegramRecording; +// @end @interface OngoingCallConnectionDescriptionWebrtc : NSObject diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 66b62848053..363fba9d93e 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -149,6 +149,33 @@ - (void)setTone:(CallAudioTone * _Nullable)tone { }); } +// MARK: Nicegram NCG-5828 call recording +-(void)StartNicegramRecording:(void(^_Nullable)(NSString* _Nonnull, double, NSUInteger))completion + errorCallback:(void (^_Nullable)(NSString* _Nonnull))errorCallback { + _audioDeviceModule->perform([completion, errorCallback](tgcalls::SharedAudioDeviceModule *audioDeviceModule) { + audioDeviceModule->audioDeviceModule()->StartNicegramRecording([completion](const std::string& outputFilePath, + double durationInSeconds, + size_t rawDataSize) { + NSString *path = [NSString stringWithUTF8String: outputFilePath.c_str()]; + if (completion != NULL) { + completion(path, durationInSeconds, rawDataSize); + } + }, [errorCallback](const std::string& error){ + NSString *message = [NSString stringWithUTF8String: error.c_str()]; + if (errorCallback != NULL) { + errorCallback(message); + } + }); + }); +} + +-(void)StopNicegramRecording { + _audioDeviceModule->perform([](tgcalls::SharedAudioDeviceModule *audioDeviceModule) { + audioDeviceModule->audioDeviceModule()->StopNicegramRecording(); + }); +} +// + - (std::shared_ptr>)getAudioDeviceModule { return _audioDeviceModule; } @@ -1781,7 +1808,11 @@ - (instancetype _Nonnull)initWithQueue:(id mapToSignal { cached in - if let cached, cached.baseLang == baseLang { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 { if !dontTranslateLanguages.contains(cached.fromLang) { return .single(cached) } else { @@ -246,20 +255,10 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) languageRecognizer.processString(text) let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 4) languageRecognizer.reset() - - func normalize(_ code: String) -> String { - if code.contains("-") { - return code.components(separatedBy: "-").first ?? code - } else if code == "nb" { - return "no" - } else { - return code - } - } - - let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalize($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) + + let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalizeTranslationLanguage($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) if let language = filteredLanguages.first { - let fromLang = normalize(language.key.rawValue) + let fromLang = normalizeTranslationLanguage(language.key.rawValue) if loggingEnabled && !["en", "ru"].contains(fromLang) && !dontTranslateLanguages.contains(fromLang) { Logger.shared.log("ChatTranslation", "\(text)") Logger.shared.log("ChatTranslation", "Recognized as: \(fromLang), other hypotheses: \(hypotheses.map { $0.key.rawValue }.joined(separator: ",")) ") @@ -287,7 +286,13 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) if loggingEnabled { Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)") } - let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false) + let state = ChatTranslationState( + baseLang: baseLang, + fromLang: fromLang, + timestamp: currentTime, + toLang: cached?.toLang, + isEnabled: cached?.isEnabled ?? false + ) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() if !dontTranslateLanguages.contains(fromLang) { return state diff --git a/submodules/TranslateUI/Sources/Translate.swift b/submodules/TranslateUI/Sources/Translate.swift index 385858f3986..64e664327b3 100644 --- a/submodules/TranslateUI/Sources/Translate.swift +++ b/submodules/TranslateUI/Sources/Translate.swift @@ -157,6 +157,17 @@ public func effectiveIgnoredTranslationLanguages(context: AccountContext, ignore return dontTranslateLanguages } +public func normalizeTranslationLanguage(_ code: String) -> String { + var code = code + if code.contains("-") { + code = code.components(separatedBy: "-").first ?? code + } + if code == "nb" { + code = "no" + } + return code +} + public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, showTranslateIfTopical: Bool = false, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) { guard showTranslate || showTranslateIfTopical, text.count > 0 else { return (false, nil) @@ -178,20 +189,10 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat if !showTranslate && showTranslateIfTopical { supportedTranslationLanguages = ["uk", "ru"] } - - func normalize(_ code: String) -> String { - if code.contains("-") { - return code.components(separatedBy: "-").first ?? code - } else if code == "nb" { - return "no" - } else { - return code - } - } - - let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalize($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) + + let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalizeTranslationLanguage($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) if let language = filteredLanguages.first { - let languageCode = normalize(language.key.rawValue) + let languageCode = normalizeTranslationLanguage(language.key.rawValue) return (!dontTranslateLanguages.contains(languageCode), languageCode) } else { return (false, nil) diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index db02dfa65d3..776763d2b51 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -1022,11 +1022,7 @@ public class TranslateScreen: ViewController { } } - if toLanguage == "nb" { - toLanguage = "no" - } else if toLanguage == "pt-br" { - toLanguage = "pt" - } + toLanguage = normalizeTranslationLanguage(toLanguage) var copyTranslationImpl: ((String) -> Void)? var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)? diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 17cd23ad5fe..3f52818db03 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/PhotoResources", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 3aa34a34058..ca2d9acc38a 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -45,9 +45,11 @@ public enum UndoOverlayContent { case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?) + case universalImage(image: UIImage, size: CGSize?, title: String?, text: String, customUndoText: String?, timeout: Double?) case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, customUndoText: String?) + case media(context: AccountContext, file: FileMediaReference, title: String?, text: String, undoText: String?, customAction: (() -> Void)?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 4705c4afc64..d04b022b4d6 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -23,6 +23,7 @@ import TextNodeWithEntities import BundleIconComponent import AnimatedTextComponent import ComponentDisplayAdapters +import PhotoResources final class UndoOverlayControllerNode: ViewControllerTracingNode { private let presentationData: PresentationData @@ -42,6 +43,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var slotMachineNode: SlotMachineAnimationNode? private var stillStickerNode: TransformImageNode? private var stickerImageSize: CGSize? + private var stickerSourceSize: CGSize? private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? private let titleNode: ImmediateTextNode @@ -1066,6 +1068,56 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + case let .universalImage(image, size, title, text, customUndoText, timeout): + self.iconNode = ASImageNode() + self.iconNode?.displayWithoutProcessing = true + self.iconNode?.displaysAsynchronously = false + self.iconNode?.image = image + self.iconImageSize = size + + self.avatarNode = nil + self.iconCheckNode = nil + self.animationNode = nil + self.animatedStickerNode = nil + + if let title = title, text.isEmpty { + self.titleNode.attributedText = nil + let body = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } else { + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } + + if text.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = timeout ?? (isUserInteractionEnabled ? 5 : 3) + + self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { undoText = customUndoText displayUndo = true @@ -1247,6 +1299,58 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { displayUndo = false } + case let .media(context, file, title, text, customUndoText, _): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let stillStickerNode = TransformImageNode() + + self.stillStickerNode = stillStickerNode + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + updatedImageSignal = mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .other, videoReference: file, onlyFullSize: false, useLargeThumbnail: false, autoFetchFullSizeThumbnail: false) + updatedFetchSignal = nil + self.stickerImageSize = CGSize(width: 30.0, height: 30.0) + self.stickerSourceSize = file.media.dimensions?.cgSize + + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 5 + + if text.contains("](") { + isUserInteractionEnabled = true + } + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 + + if let updatedFetchSignal = updatedFetchSignal { + self.fetchResourceDisposable = updatedFetchSignal.start() + } + + if let updatedImageSignal = updatedImageSignal { + stillStickerNode.setSignal(updatedImageSignal) + } } self.remainingSeconds = self.originalRemainingSeconds @@ -1284,13 +1388,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal,. universalImage, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { self.isUserInteractionEnabled = false } - case .sticker, .customEmoji: + case .sticker, .customEmoji, .media: self.isUserInteractionEnabled = displayUndo case .dice: self.panelWrapperNode.clipsToBounds = true @@ -1418,6 +1522,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { let _ = self.action(.undo) } + case let .media(_, _, _, _, _, customAction): + if let customAction = customAction { + customAction() + } else { + let _ = self.action(.undo) + } default: let _ = self.action(.undo) } @@ -1725,7 +1835,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if let stillStickerNode = self.stillStickerNode { let makeImageLayout = stillStickerNode.asyncLayout() - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stickerImageSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) + + var radius: CGFloat = 0.0 + if case .media = self.content { + radius = 6.0 + } + var stickerImageSourceSize = stickerImageSize + if let stickerSourceSize = self.stickerSourceSize { + stickerImageSourceSize = stickerSourceSize.aspectFilled(stickerImageSourceSize) + } + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: stickerImageSourceSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) let _ = imageApply() transition.updateFrame(node: stillStickerNode, frame: iconFrame) } diff --git a/submodules/Utils/DeviceModel/BUILD b/submodules/Utils/DeviceModel/BUILD index 16d3d3f3563..14370c9c437 100644 --- a/submodules/Utils/DeviceModel/BUILD +++ b/submodules/Utils/DeviceModel/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), copts = [ - "-warnings-as-errors", + #"-warnings-as-errors", ], deps = [ "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift index ba30871c3b0..e74039e14ec 100644 --- a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -48,7 +48,11 @@ public enum DeviceModel: CaseIterable, Equatable { .iPhone15, .iPhone15Plus, .iPhone15Pro, - .iPhone15ProMax + .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, + .iPhone16Pro, + .iPhone16ProMax ] } @@ -116,6 +120,11 @@ public enum DeviceModel: CaseIterable, Equatable { case iPhone15Pro case iPhone15ProMax + case iPhone16 + case iPhone16Plus + case iPhone16Pro + case iPhone16ProMax + case unknown(String) public var modelId: [String] { @@ -218,6 +227,14 @@ public enum DeviceModel: CaseIterable, Equatable { return ["iPhone16,1"] case .iPhone15ProMax: return ["iPhone16,2"] + case .iPhone16: + return ["iPhone17,3"] + case .iPhone16Plus: + return ["iPhone17,4"] + case .iPhone16Pro: + return ["iPhone17,1"] + case .iPhone16ProMax: + return ["iPhone17,2"] case let .unknown(modelId): return [modelId] } @@ -323,6 +340,14 @@ public enum DeviceModel: CaseIterable, Equatable { return "iPhone 15 Pro" case .iPhone15ProMax: return "iPhone 15 Pro Max" + case .iPhone16: + return "iPhone 16" + case .iPhone16Plus: + return "iPhone 16 Plus" + case .iPhone16Pro: + return "iPhone 16 Pro" + case .iPhone16ProMax: + return "iPhone 16 Pro Max" case let .unknown(modelId): if modelId.hasPrefix("iPhone") { return "Unknown iPhone" diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index a07d619330a..d13355b17bf 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -697,23 +697,6 @@ public func patternColor(for color: UIColor, intensity: CGFloat, prominent: Bool return .black } -public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return .single({ arguments in - guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { - return nil - } - - context.withFlippedContext { c in - c.setFillColor(color.withAlphaComponent(1.0).cgColor) - c.fill(arguments.drawingRect) - } - - addCorners(context, arguments: arguments) - - return context - }) -} - public func drawWallpaperGradientImage(_ colors: [UIColor], rotation: Int32? = nil, context: CGContext, size: CGSize) { guard !colors.isEmpty else { return diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index c7c75e42e16..8f88b40ff73 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -172,7 +172,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P for attribute in file.attributes { switch attribute { - case let .Video(duration, size, flags, _, _): + case let .Video(duration, size, flags, _, _, _): bridgeVideo.duration = Int32(duration) bridgeVideo.dimensions = size.cgSize bridgeVideo.round = flags.contains(.instantRoundVideo) diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 5c986acfb2c..964990c99b9 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -720,7 +720,7 @@ final class WatchAudioHandler: WatchRequestHandler { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) } - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() } }) } else { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index f6b47151423..b9e7363af99 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -37,7 +37,7 @@ struct WebSearchGalleryEntry: Equatable { switch self.result { case let .externalReference(externalReference): if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions { - let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) } case let .internalReference(internalReference): diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index 142323c37d7..d3696d5d0d8 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -165,7 +165,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) videoNode.updateLayout(size: videoSize, transition: .immediate) self.videoNode = videoNode diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index a52788f54e6..ff35b5b3389 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -3,7 +3,7 @@ import NicegramWallet // import Foundation import UIKit -import WebKit +@preconcurrency import WebKit import Display import AsyncDisplayKit import Postbox @@ -474,7 +474,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if isTelegramMeLink(url) || isTelegraPhLink(url) { decisionHandler(.cancel) - self.controller?.openUrl(url, true, {}) + self.controller?.openUrl(url, true, false, {}) } else { decisionHandler(.allow) } @@ -491,7 +491,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } // - self.controller?.openUrl(url.absoluteString, true, {}) + self.controller?.openUrl(url.absoluteString, true, false, {}) } return nil } @@ -615,6 +615,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } + + private var updateWebViewWhenStable = false func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousLayout = self.validLayout?.0 @@ -632,7 +634,10 @@ public final class WebAppController: ViewController, AttachmentContainable { scrollInset.bottom = 0.0 } - let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) + let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) + if !webView.frame.width.isZero && webView.frame != frame { + self.updateWebViewWhenStable = true + } var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { @@ -663,9 +668,18 @@ public final class WebAppController: ViewController, AttachmentContainable { if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) + if self.updateWebViewWhenStable && !controller.isContainerPanning() { + self.updateWebViewWhenStable = false + webView.setNeedsLayout() + } } - webView.customBottomInset = layout.intrinsicInsets.bottom + if layout.intrinsicInsets.bottom > 44.0 { + webView.customBottomInset = 0.0 + } else { + webView.customBottomInset = layout.intrinsicInsets.bottom + } + webView.customSideInset = layout.safeInsets.left } if let placeholderNode = self.placeholderNode { @@ -816,7 +830,8 @@ public final class WebAppController: ViewController, AttachmentContainable { controller.dismiss() case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { - controller.openUrl("https://t.me\(path)", false, { [weak controller] in + let forceRequest = json["force_request"] as? Bool ?? false + controller.openUrl("https://t.me\(path)", false, forceRequest, { [weak controller] in let _ = controller // controller?.dismiss() }) @@ -1902,7 +1917,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private var hasSettings = false - public var openUrl: (String, Bool, @escaping () -> Void) -> Void = { _, _, _ in } + public var openUrl: (String, Bool, Bool, @escaping () -> Void) -> Void = { _, _, _, _ in } public var getNavigationController: () -> NavigationController? = { return nil } public var completion: () -> Void = {} public var requestSwitchInline: (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in } @@ -2154,7 +2169,7 @@ public final class WebAppController: ViewController, AttachmentContainable { let context = self.context let _ = (cachedWebAppTermsPage(context: context) |> deliverOnMainQueue).startStandalone(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: true, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: true, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] c, arguments in self?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -2383,7 +2398,7 @@ public func standaloneWebAppController( updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, params: WebAppParameters, threadId: Int64?, - openUrl: @escaping (String, Bool, @escaping () -> Void) -> Void, + openUrl: @escaping (String, Bool, Bool, @escaping () -> Void) -> Void, requestSwitchInline: @escaping (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in }, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, diff --git a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift index 534d9776429..263e8c9b011 100644 --- a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift +++ b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display +import ComponentFlow import Postbox import TelegramCore import TelegramPresentationData @@ -12,6 +13,7 @@ import AppBundle import AvatarNode import CheckNode import Markdown +import EmojiStatusComponent private let textFont = Font.regular(13.0) private let boldTextFont = Font.semibold(13.0) @@ -21,6 +23,8 @@ private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, t } private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { + private let context: AccountContext + private let presentationTheme: PresentationTheme private let strings: PresentationStrings private let peer: EnginePeer private let title: String @@ -28,6 +32,7 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { private let showMore: Bool private let titleNode: ImmediateTextNode + private var titleCredibilityIconView: ComponentHostView? private let textNode: ASTextNode private let avatarNode: AvatarNode @@ -57,7 +62,9 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void, termsPressed: @escaping () -> Void) { + self.context = context self.strings = strings + self.presentationTheme = ptheme self.peer = peer self.title = title self.text = text @@ -209,7 +216,42 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + var totalWidth = titleSize.width + + if self.peer.isVerified { + let statusContent: EmojiStatusComponent.Content = .verified(fillColor: self.presentationTheme.list.itemCheckColors.fillColor, foregroundColor: self.presentationTheme.list.itemCheckColors.foregroundColor, sizeType: .large) + let titleCredibilityIconTransition: ComponentTransition = .immediate + + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.view.addSubview(titleCredibilityIconView) + } + + let titleIconSize = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: statusContent, + isVisibleForAnimations: true, + action: { + } + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + totalWidth += titleIconSize.width + 2.0 + + titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x:floorToScreenPixels((size.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: origin.y), size: titleIconSize)) + } + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: origin.y), size: titleSize)) origin.y += titleSize.height + 6.0 var entriesHeight: CGFloat = 0.0 diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index ce602c1b187..20956490afd 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -48,7 +48,7 @@ private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(in " style.appendChild(document.createTextNode(css)); head.appendChild(style);" private let videoSource = """ -function disableWebkitEnterFullscreen(videoElement) { +function tgBrowserDisableWebkitEnterFullscreen(videoElement) { if (videoElement && videoElement.webkitEnterFullscreen) { Object.defineProperty(videoElement, 'webkitEnterFullscreen', { value: undefined @@ -56,11 +56,11 @@ function disableWebkitEnterFullscreen(videoElement) { } } -function disableFullscreenOnExistingVideos() { - document.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); +function tgBrowserDisableFullscreenOnExistingVideos() { + document.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen); } -function handleMutations(mutations) { +function tgBrowserHandleMutations(mutations) { mutations.forEach((mutation) => { if (mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((newNode) => { @@ -75,22 +75,30 @@ function handleMutations(mutations) { }); } -disableFullscreenOnExistingVideos(); +tgBrowserDisableFullscreenOnExistingVideos(); -const observer = new MutationObserver(handleMutations); +const _tgbrowser_observer = new MutationObserver(tgBrowserHandleMutations); -observer.observe(document.body, { +_tgbrowser_observer.observe(document.body, { childList: true, subtree: true }); -function disconnectObserver() { - observer.disconnect(); +function tgBrowserDisconnectObserver() { + _tgbrowser_observer.disconnect(); } """ final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } + + var customSideInset: CGFloat = 0.0 { + didSet { + if self.customSideInset != oldValue { + self.setNeedsLayout() + } + } + } var customBottomInset: CGFloat = 0.0 { didSet { @@ -101,7 +109,7 @@ final class WebAppWebView: WKWebView { } override var safeAreaInsets: UIEdgeInsets { - return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) + return UIEdgeInsets(top: 0.0, left: self.customSideInset, bottom: self.customBottomInset, right: self.customSideInset) } init(account: Account) { diff --git a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift index e350f2ec7cc..c643a025e07 100644 --- a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift +++ b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift @@ -22,7 +22,7 @@ public extension WidgetDataPeer.Message { switch attribute { case let .Sticker(altText, _, _): content = .sticker(WidgetDataPeer.Message.Content.Sticker(altText: altText)) - case let .Video(duration, _, flags, _, _): + case let .Video(duration, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { content = .videoMessage(WidgetDataPeer.Message.Content.VideoMessage(duration: Int32(duration))) } else { diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index be58ac5e4f9..31bfa57c0af 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -48,7 +48,7 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libvpx \ --enable-audiotoolbox \ --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ - --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ + --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at,vorbis \ --enable-encoder=libvpx_vp9,aac_at \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-parser=aac,h264,mp3,libopus \ diff --git a/third-party/webrtc/webrtc b/third-party/webrtc/webrtc index c75f2a397ec..41a3014d3e0 160000 --- a/third-party/webrtc/webrtc +++ b/third-party/webrtc/webrtc @@ -1 +1 @@ -Subproject commit c75f2a397ec3c7db12677b39b52d2b3f8ee9161e +Subproject commit 41a3014d3e09c9fc63e563b34fd586df0d5d105d diff --git a/versions.json b/versions.json index 8e01f636a52..f82f16dd2d6 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { - "app": "1.8.5", - "xcode": "15.2", - "bazel": "7.1.1", - "macos": "13.0" + "app": "1.8.8", + "xcode": "16.0", + "bazel": "7.3.1", + "macos": "15.0" }