Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mikhail Filimonov 2025-05-06 08:20:21 +01:00
commit ecb1711970
89 changed files with 3777 additions and 2047 deletions

View File

@ -1 +1,2 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
spm-files

1
.gitignore vendored
View File

@ -73,3 +73,4 @@ buildServer.json
.build/**
Telegram.LSP.json
**/.build/**
spm-files

View File

@ -11,5 +11,8 @@
},
"search.exclude": {
".git/**": true
},
"files.associations": {
"memory": "cpp"
}
}

View File

@ -55,6 +55,10 @@ load("@build_bazel_rules_apple//apple:resources.bzl",
"swift_intent_library",
)
load("//build-system/bazel-utils:spm.bzl",
"generate_spm",
)
config_setting(
name = "debug",
values = {
@ -952,29 +956,6 @@ plist_fragment(
)
)
ios_framework(
name = "TelegramApiFramework",
bundle_id = "{telegram_bundle_id}.TelegramApi".format(
telegram_bundle_id = telegram_bundle_id,
),
families = [
"iphone",
"ipad",
],
infoplists = [
":TelegramApiInfoPlist",
":BuildNumberInfoPlist",
":VersionInfoPlist",
":RequiredDeviceCapabilitiesPlist",
],
minimum_os_version = minimum_os_version,
extension_safe = True,
ipa_post_processor = strip_framework,
deps = [
"//submodules/TelegramApi:TelegramApi",
],
)
plist_fragment(
name = "TelegramCoreInfoPlist",
extension = "plist",
@ -2022,7 +2003,45 @@ xcodeproj(
default_xcode_configuration = "Debug"
)
# Temporary targets used to simplify webrtc build tests
# Temporary targets used to simplify build tests
ios_application(
name = "spm_build_app",
bundle_id = "{telegram_bundle_id}".format(
telegram_bundle_id = telegram_bundle_id,
),
families = ["iphone", "ipad"],
minimum_os_version = minimum_os_version,
provisioning_profile = select({
":disableProvisioningProfilesSetting": None,
"//conditions:default": "@build_configuration//provisioning:Telegram.mobileprovision",
}),
entitlements = ":TelegramEntitlements.entitlements",
infoplists = [
":TelegramInfoPlist",
":BuildNumberInfoPlist",
":VersionInfoPlist",
":RequiredDeviceCapabilitiesPlist",
":UrlTypesInfoPlist",
],
deps = [
#"//submodules/MtProtoKit",
#"//submodules/SSignalKit/SwiftSignalKit",
#"//submodules/Postbox",
#"//submodules/TelegramApi",
#"//submodules/TelegramCore",
#"//submodules/FFMpegBinding",
"//submodules/Display",
#"//third-party/webrtc",
],
)
generate_spm(
name = "spm_build_root",
deps = [
":spm_build_app",
]
)
ios_application(
name = "webrtc_build_test",
@ -2044,7 +2063,7 @@ ios_application(
":UrlTypesInfoPlist",
],
deps = [
"//third-party/webrtc:webrtc_lib",
"//third-party/webrtc:webrtc",
],
)

View File

@ -336,7 +336,7 @@ private final class EmbeddedBroadcastUploadImpl: BroadcastUploadImpl {
let logsPath = rootPath + "/logs/broadcast-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
let embeddedBroadcastImplementationTypePath = rootPath + "/broadcast-coordination-type"
let embeddedBroadcastImplementationTypePath = rootPath + "/broadcast-coordination-type-v2"
var useIPCContext = false
if let typeData = try? Data(contentsOf: URL(fileURLWithPath: embeddedBroadcastImplementationTypePath)), let type = String(data: typeData, encoding: .utf8) {

View File

@ -14262,6 +14262,7 @@ Sorry for the inconvenience.";
"PeerInfo.Gifts.Sale" = "sale";
"Gift.Store.ForResaleNoResults" = "no results";
"Gift.Store.ForResale_1" = "%@ for resale";
"Gift.Store.ForResale_any" = "%@ for resale";
"Gift.Store.Sort.Price" = "Price";
@ -14291,8 +14292,31 @@ Sorry for the inconvenience.";
"MediaPicker.CreateStory_any" = "Create %@ Stories";
"MediaPicker.CombineIntoCollage" = "Combine into Collage";
"Gift.Resale.Unavailable.Title" = "Resell Gift";
"Gift.Resale.Unavailable.Text" = "Sorry, you can't list this gift yet.\n\Reselling will be available on %@.";
"Gift.Resale.Unavailable.Title" = "Try Later";
"Gift.Resale.Unavailable.Text" = "You will be able to resell this gift on %@.";
"Gift.Transfer.Unavailable.Title" = "Transfer Gift";
"Gift.Transfer.Unavailable.Text" = "Sorry, you can't transfer this gift yet.\n\Transferring will be available on %@.";
"Gift.Transfer.Unavailable.Title" = "Try Later";
"Gift.Transfer.Unavailable.Text" = "You will be able to transfer this gift on %@.";
"Premium.CreateMultipleStories" = "Create Multiple Stories";
"FrozenAccount.Violation.TextNew" = "Your account was frozen for breaking Telegram's [Terms and Conditions]().";
"FrozenAccount.Violation.TextNew_URL" = "https://telegram.org/tos";
"Stars.Purchase.BuyStarGiftInfo" = "Buy Stars to acquire a unique collectible.";
"Stars.Purchase.EnoughStars" = "You have enough stars at the moment.";
"Stars.Purchase.BuyAnyway" = "Buy Anyway";
"Gift.Buy.Confirm.Title" = "Confirm Payment";
"Gift.Buy.Confirm.Text" = "Do you really want to buy **%1$@** for %2$@?";
"Gift.Buy.Confirm.GiftText" = "Do you really want to buy **%1$@** for %2$@ and gift it to **%3$@**?";
"Gift.Buy.Confirm.Text.Stars_1" = "**%@** Star";
"Gift.Buy.Confirm.Text.Stars_any" = "**%@** Stars";
"Gift.Buy.Confirm.BuyFor_1" = "Buy for %@ Star";
"Gift.Buy.Confirm.BuyFor_any" = "Buy for %@ Stars";
"Calls.HideCallsTab" = "Hide Calls Tab";
"Story.Editor.TooltipSelection_1" = "Tap here to view your %@ story";
"Story.Editor.TooltipSelection_any" = "Tap here to view your %@ stories";

View File

@ -35,7 +35,8 @@ public final class ViewController: UIViewController {
isRemoteAudioMuted: false,
localVideo: nil,
remoteVideo: nil,
isRemoteBatteryLow: false
isRemoteBatteryLow: false,
enableVideoSharpening: false
)
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?

View File

@ -393,6 +393,58 @@ class BazelCommandLine:
print(subprocess.list2cmdline(combined_arguments))
call_executable(combined_arguments)
def get_spm_aspect_invocation(self):
combined_arguments = [
self.build_environment.bazel_path
]
combined_arguments += self.get_startup_bazel_arguments()
combined_arguments += ['build']
if self.custom_target is not None:
combined_arguments += [self.custom_target]
else:
combined_arguments += ['Telegram/Telegram']
if self.continue_on_error:
combined_arguments += ['--keep_going']
if self.show_actions:
combined_arguments += ['--subcommands']
if self.enable_sandbox:
combined_arguments += ['--spawn_strategy=sandboxed']
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.configuration_path is None:
raise Exception('configuration_path is not defined')
combined_arguments += [
'--override_repository=build_configuration={}'.format(self.configuration_path)
]
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
combined_arguments += self.get_additional_build_arguments()
if self.remote_cache is not None:
combined_arguments += [
'--remote_cache={}'.format(self.remote_cache),
'--experimental_remote_downloader={}'.format(self.remote_cache)
]
elif self.cache_dir is not None:
combined_arguments += [
'--disk_cache={path}'.format(path=self.cache_dir)
]
combined_arguments += self.configuration_args
combined_arguments += ['--aspects', '//build-system/bazel-utils:spm.bzl%spm_text_aspect']
print(subprocess.list2cmdline(combined_arguments))
call_executable(combined_arguments)
def clean(bazel, arguments):
bazel_command_line = BazelCommandLine(
@ -696,6 +748,36 @@ def query(bazel, arguments):
bazel_command_line.invoke_query(query_args)
def get_spm_aspect_invocation(bazel, arguments):
bazel_command_line = BazelCommandLine(
bazel=bazel,
override_bazel_version=arguments.overrideBazelVersion,
override_xcode_version=arguments.overrideXcodeVersion,
bazel_user_root=arguments.bazelUserRoot
)
if arguments.cacheDir is not None:
bazel_command_line.add_cache_dir(arguments.cacheDir)
elif arguments.cacheHost is not None:
bazel_command_line.add_remote_cache(arguments.cacheHost)
resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=bazel_command_line,
arguments=arguments,
additional_codesigning_output_path=None
)
bazel_command_line.set_configuration(arguments.configuration)
bazel_command_line.set_build_number(arguments.buildNumber)
bazel_command_line.set_custom_target(arguments.target)
bazel_command_line.set_continue_on_error(False)
bazel_command_line.set_show_actions(False)
bazel_command_line.set_enable_sandbox(False)
bazel_command_line.set_split_swiftmodules(False)
bazel_command_line.get_spm_aspect_invocation()
def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
configuration_group = current_parser.add_mutually_exclusive_group(required=True)
configuration_group.add_argument(
@ -1121,6 +1203,38 @@ if __name__ == '__main__':
metavar='query_string'
)
spm_parser = subparsers.add_parser('spm', help='Generate SPM package')
spm_parser.add_argument(
'--target',
type=str,
help='A custom bazel target name to build.',
metavar='target_name'
)
spm_parser.add_argument(
'--buildNumber',
required=False,
type=int,
default=10000,
help='Build number.',
metavar='number'
)
spm_parser.add_argument(
'--configuration',
choices=[
'debug_universal',
'debug_arm64',
'debug_armv7',
'debug_sim_arm64',
'release_sim_arm64',
'release_arm64',
'release_armv7',
'release_universal'
],
required=True,
help='Build configuration'
)
add_codesigning_common_arguments(spm_parser)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
@ -1229,6 +1343,8 @@ if __name__ == '__main__':
test(bazel=bazel_path, arguments=args)
elif args.commandName == 'query':
query(bazel=bazel_path, arguments=args)
elif args.commandName == 'spm':
get_spm_aspect_invocation(bazel=bazel_path, arguments=args)
else:
raise Exception('Unknown command')
except KeyboardInterrupt:

@ -1 +1 @@
Subproject commit 44b6f046d95b84933c1149fbf7f9d81fd4e32020
Subproject commit 41929acc4c7c1da973c77871d0375207b9d0806f

View File

@ -0,0 +1,447 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "SwiftInfo")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//lib:dicts.bzl", "dicts")
# Define provider to propagate data
SPMModulesInfo = provider(
fields = {
"modules": "Dictionary of module information",
"transitive_sources": "Depset of all transitive source files",
}
)
_IGNORE_CC_LIBRARY_ATTRS = [
"data",
"applicable_licenses",
"alwayslink",
"aspect_hints",
"compatible_with",
"deprecation",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generator_function",
"generator_location",
"generator_name",
"generator_platform",
"generator_script",
"generator_tool",
"generator_toolchain",
"generator_toolchain_type",
"licenses",
"linkstamp",
"linkstatic",
"name",
"restricted_to",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
"win_def_file",
"linkopts",
]
_IGNORE_CC_LIBRARY_EMPTY_ATTRS = [
"additional_compiler_inputs",
"additional_linker_inputs",
"hdrs_check",
"implementation_deps",
"include_prefix",
"strip_include_prefix",
"local_defines",
]
_CC_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"hdrs": [],
"includes": [],
"srcs": [],
"textual_hdrs": [],
}
_CC_LIBRARY_REQUIRED_ATTRS = {
}
_IGNORE_OBJC_LIBRARY_ATTRS = [
"data",
"alwayslink",
"applicable_licenses",
"aspect_hints",
"compatible_with",
"enable_modules",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generator_function",
"generator_location",
"generator_name",
"deprecation",
"module_name",
"name",
"stamp",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
]
_IGNORE_OBJC_LIBRARY_EMPTY_ATTRS = [
"implementation_deps",
"linkopts",
"module_map",
"non_arc_srcs",
"pch",
"restricted_to",
"textual_hdrs",
"sdk_includes",
]
_OBJC_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"hdrs": [],
"srcs": [],
"sdk_dylibs": [],
"sdk_frameworks": [],
"weak_sdk_frameworks": [],
"includes": [],
}
_OBJC_LIBRARY_REQUIRED_ATTRS = [
"module_name",
]
_IGNORE_SWIFT_LIBRARY_ATTRS = [
"data",
"always_include_developer_search_paths",
"alwayslink",
"applicable_licenses",
"aspect_hints",
"compatible_with",
"deprecation",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generated_header_name",
"generates_header",
"generator_function",
"generator_location",
"generator_name",
"linkstatic",
"module_name",
"name",
"package_name",
"restricted_to",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
]
_IGNORE_SWIFT_LIBRARY_EMPTY_ATTRS = [
"plugins",
"private_deps",
"swiftc_inputs",
]
_SWIFT_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"linkopts": [],
"srcs": [],
}
_SWIFT_LIBRARY_REQUIRED_ATTRS = [
"module_name",
]
_LIBRARY_CONFIGS = {
"cc_library": {
"ignore_attrs": _IGNORE_CC_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_CC_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _CC_LIBRARY_ATTRS,
"required_attrs": _CC_LIBRARY_REQUIRED_ATTRS,
},
"objc_library": {
"ignore_attrs": _IGNORE_OBJC_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_OBJC_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _OBJC_LIBRARY_ATTRS,
"required_attrs": _OBJC_LIBRARY_REQUIRED_ATTRS,
},
"swift_library": {
"ignore_attrs": _IGNORE_SWIFT_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_SWIFT_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _SWIFT_LIBRARY_ATTRS,
"required_attrs": _SWIFT_LIBRARY_REQUIRED_ATTRS,
},
}
def get_rule_atts(rule):
if rule.kind in _LIBRARY_CONFIGS:
config = _LIBRARY_CONFIGS[rule.kind]
ignore_attrs = config["ignore_attrs"]
ignore_empty_attrs = config["ignore_empty_attrs"]
handled_attrs = config["handled_attrs"]
required_attrs = config["required_attrs"]
for attr_name in dir(rule.attr):
if attr_name.startswith("_"):
continue
if attr_name in ignore_attrs:
continue
if attr_name in ignore_empty_attrs:
attr_value = getattr(rule.attr, attr_name)
if attr_value == [] or attr_value == None or attr_value == "":
continue
else:
fail("Attribute {} is not empty: {}".format(attr_name, attr_value))
if attr_name in handled_attrs:
continue
fail("Unknown attribute: {}".format(attr_name))
result = dict()
result["type"] = rule.kind
for attr_name in handled_attrs:
if hasattr(rule.attr, attr_name):
result[attr_name] = getattr(rule.attr, attr_name)
else:
result[attr_name] = handled_attrs[attr_name] # Use default value
for attr_name in required_attrs:
if not hasattr(rule.attr, attr_name):
if rule.kind == "objc_library" and attr_name == "module_name":
result[attr_name] = getattr(rule.attr, "name")
else:
fail("Required attribute {} is missing".format(attr_name))
else:
result[attr_name] = getattr(rule.attr, attr_name)
result["name"] = getattr(rule.attr, "name")
return result
elif rule.kind == "ios_application":
result = dict()
result["type"] = "ios_application"
return result
elif rule.kind == "generate_spm":
result = dict()
result["type"] = "root"
return result
elif rule.kind == "apple_static_xcframework_import":
result = dict()
result["type"] = "apple_static_xcframework_import"
return result
else:
fail("Unknown rule kind: {}".format(rule.kind))
def _collect_spm_modules_impl(target, ctx):
# Skip targets without DefaultInfo
if not DefaultInfo in target:
return []
# Get module name
module_name = ctx.label.name
if hasattr(ctx.rule.attr, "module_name"):
module_name = ctx.rule.attr.module_name or ctx.label.name
# Collect all modules and transitive sources from dependencies first
all_modules = {}
dep_transitive_sources_list = []
if hasattr(ctx.rule.attr, "deps"):
for dep in ctx.rule.attr.deps:
if SPMModulesInfo in dep:
# Merge the modules dictionaries
for label, info in dep[SPMModulesInfo].modules.items():
all_modules[label] = info
# Add transitive sources depset from dependency to the list
dep_transitive_sources_list.append(dep[SPMModulesInfo].transitive_sources)
# Merge all transitive sources from dependencies
transitive_sources_from_deps = depset(transitive = dep_transitive_sources_list)
# Keep this for debugging later
# if result_attrs["type"] == "swift_library":
# print("Processing rule {}".format(ctx.label.name))
# print("ctx.rule.kind = {}".format(ctx.rule.kind))
# for attr_name in dir(ctx.rule.attr):
# print(" attr1: {}".format(attr_name))
result_attrs = get_rule_atts(ctx.rule)
sources = []
current_target_src_files = []
if "srcs" in result_attrs:
for src_target in result_attrs["srcs"]:
src_files = src_target.files.to_list()
for f in src_files:
if f.extension in ["swift", "cc", "cpp", "h", "m", "mm", "s", "S"]:
current_target_src_files.append(f)
for src_file in src_files:
sources.append(src_file.path)
current_target_sources = depset(current_target_src_files)
headers = []
current_target_hdr_files = []
if "hdrs" in result_attrs:
for hdr_target in result_attrs["hdrs"]:
hdr_files = hdr_target.files.to_list()
for f in hdr_files:
current_target_hdr_files.append(f)
for hdr_file in hdr_files:
headers.append(hdr_file.path)
current_target_headers = depset(current_target_hdr_files)
module_type = result_attrs["type"]
if module_type == "root":
pass
elif module_type == "apple_static_xcframework_import":
pass
elif module_type == "objc_library" or module_type == "swift_library" or module_type == "cc_library":
# Collect dependency labels
dep_names = []
if "deps" in result_attrs:
for dep in result_attrs["deps"]:
if hasattr(dep, "label"):
dep_label = str(dep.label)
dep_name = dep_label.split(":")[-1]
dep_names.append(dep_name)
else:
fail("Missing dependency label")
if module_type == "objc_library" or module_type == "swift_library":
if result_attrs["module_name"] != result_attrs["name"]:
fail("Module name mismatch: {} != {}".format(result_attrs["module_name"], result_attrs["name"]))
# Extract the path from the label
# Example: @//path/ModuleName:ModuleSubname -> path/ModuleName
if not str(ctx.label).startswith("@//"):
fail("Invalid label: {}".format(ctx.label))
module_path = str(ctx.label).split(":")[0].split("@//")[1]
if module_type == "objc_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"defines": result_attrs["defines"],
"deps": dep_names,
"sources": sorted(sources + headers),
"module_name": module_name,
"copts": result_attrs["copts"],
"sdk_frameworks": result_attrs["sdk_frameworks"],
"sdk_dylibs": result_attrs["sdk_dylibs"],
"weak_sdk_frameworks": result_attrs["weak_sdk_frameworks"],
"includes": result_attrs["includes"],
}
elif module_type == "cc_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"defines": result_attrs["defines"],
"deps": dep_names,
"sources": sorted(sources + headers),
"module_name": module_name,
"copts": result_attrs["copts"],
"includes": result_attrs["includes"],
}
elif module_type == "swift_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"deps": dep_names,
"sources": sorted(sources),
"module_name": module_name,
"copts": result_attrs["copts"],
}
else:
fail("Unknown module type: {}".format(module_type))
if result_attrs["name"] in all_modules:
fail("Duplicate module name: {}".format(result_attrs["name"]))
all_modules[result_attrs["name"]] = module_info
elif result_attrs["type"] == "ios_application":
pass
else:
fail("Unknown rule type: {}".format(ctx.rule.kind))
# Add current target's sources and headers to the transitive set
final_transitive_sources = depset(transitive = [
transitive_sources_from_deps,
current_target_sources,
current_target_headers,
])
# Return both the SPM output files and the provider with modules data and sources
return [
SPMModulesInfo(
modules = all_modules,
transitive_sources = final_transitive_sources,
),
]
spm_modules_aspect = aspect(
implementation = _collect_spm_modules_impl,
attr_aspects = ["deps"],
)
def _generate_spm_impl(ctx):
outputs = []
dep_transitive_sources_list = []
if len(ctx.attr.deps) != 1:
fail("generate_spm must have exactly one dependency")
if SPMModulesInfo not in ctx.attr.deps[0]:
fail("generate_spm must have a dependency with SPMModulesInfo provider")
spm_info = ctx.attr.deps[0][SPMModulesInfo]
modules = spm_info.modules
# Declare and write the modules JSON file
modules_json_out = ctx.actions.declare_file("%s_modules.json" % ctx.label.name)
ctx.actions.write(
output = modules_json_out,
content = json.encode_indent(modules, indent = " "), # Use encode_indent for readability
)
outputs.append(modules_json_out)
for dep in ctx.attr.deps:
if SPMModulesInfo in dep:
# Add transitive sources depset from dependency
dep_transitive_sources_list.append(dep[SPMModulesInfo].transitive_sources)
# Merge all transitive sources from dependencies
transitive_sources_from_deps = depset(transitive = dep_transitive_sources_list)
# Return DefaultInfo containing only the output files in the 'files' field,
# but include the transitive sources in 'runfiles' to enforce the dependency.
return [DefaultInfo(
files = depset(outputs),
runfiles = ctx.runfiles(transitive_files = transitive_sources_from_deps),
)]
generate_spm = rule(
implementation = _generate_spm_impl,
attrs = {
'deps' : attr.label_list(aspects = [spm_modules_aspect]),
},
)

View File

@ -0,0 +1,193 @@
#! /usr/bin/env python3
import sys
import os
import sys
import json
import shutil
# Read the modules JSON file
modules_json_path = "bazel-bin/Telegram/spm_build_root_modules.json"
with open(modules_json_path, 'r') as f:
modules = json.load(f)
# Clean spm-files
spm_files_dir = "spm-files"
if os.path.exists(spm_files_dir):
shutil.rmtree(spm_files_dir)
if not os.path.exists(spm_files_dir):
os.makedirs(spm_files_dir)
combined_lines = []
combined_lines.append("// swift-tools-version: 6.0")
combined_lines.append("// The swift-tools-version declares the minimum version of Swift required to build this package.")
combined_lines.append("")
combined_lines.append("import PackageDescription")
combined_lines.append("")
combined_lines.append("let package = Package(")
combined_lines.append(" name: \"Telegram\",")
combined_lines.append(" platforms: [")
combined_lines.append(" .iOS(.v13)")
combined_lines.append(" ],")
combined_lines.append(" products: [")
for name, module in sorted(modules.items()):
if module["type"] == "objc_library" or module["type"] == "swift_library" or module["type"] == "cc_library":
combined_lines.append(" .library(name: \"%s\", targets: [\"%s\"])," % (module["name"], module["name"]))
combined_lines.append(" ],")
combined_lines.append(" targets: [")
for name, module in sorted(modules.items()):
module_type = module["type"]
if module_type == "objc_library" or module_type == "cc_library" or module_type == "swift_library":
combined_lines.append(" .target(")
combined_lines.append(" name: \"%s\"," % name)
linked_directory = None
has_non_linked_sources = False
for source in module["sources"]:
if source.startswith("bazel-out/"):
linked_directory = "spm-files/" + name
else:
has_non_linked_sources = True
if linked_directory and has_non_linked_sources:
print("Module {} has both regular and generated sources".format(name))
sys.exit(1)
if linked_directory:
os.makedirs(linked_directory)
combined_lines.append(" dependencies: [")
for dep in module["deps"]:
combined_lines.append(" .target(name: \"%s\")," % dep)
combined_lines.append(" ],")
if linked_directory:
combined_lines.append(" path: \"%s\"," % linked_directory)
else:
combined_lines.append(" path: \"%s\"," % module["path"])
combined_lines.append(" exclude: [")
exclude_files_and_dirs = []
if not linked_directory:
for root, dirs, files in os.walk(module["path"]):
rel_path = os.path.relpath(root, module["path"])
if rel_path == ".":
rel_path = ""
else:
rel_path += "/"
# Add directories that should be excluded
for d in dirs:
dir_path = os.path.join(rel_path, d)
if any(component.startswith('.') for component in dir_path.split('/')):
continue
# Check if any source file is under this directory
has_source = False
for source in module["sources"]:
rel_source = source[len(module["path"]) + 1:]
if rel_source.startswith(dir_path + "/"):
has_source = True
break
if not has_source:
exclude_files_and_dirs.append(dir_path)
# Add files that should be excluded
for f in files:
file_path = os.path.join(rel_path, f)
if any(component.startswith('.') for component in file_path.split('/')):
continue
if file_path not in [source[len(module["path"]) + 1:] for source in module["sources"]]:
exclude_files_and_dirs.append(file_path)
for item in exclude_files_and_dirs:
combined_lines.append(" \"%s\"," % item)
combined_lines.append(" ],")
combined_lines.append(" sources: [")
for source in module["sources"]:
if source.endswith(('.h', '.hpp')):
continue
linked_source_file_names = []
if not source.startswith(module["path"]):
if source.startswith("bazel-out/"):
if not linked_directory:
print("Source {} is a generated file, but module {} has no linked directory".format(source, name))
sys.exit(1)
if module["path"] in source:
source_file_name = source[source.index(module["path"]) + len(module["path"]) + 1:]
else:
print("Source {} is not inside module path {}".format(source, module["path"]))
sys.exit(1)
if source_file_name in linked_source_file_names:
print("Source {} is a duplicate".format(source))
sys.exit(1)
linked_source_file_names.append(source_file_name)
# Create any parent directories needed for the source file
symlink_location = os.path.join(linked_directory, source_file_name)
source_dir = os.path.dirname(symlink_location)
if not os.path.exists(source_dir):
os.makedirs(source_dir)
# Calculate the relative path from the symlink location back to the workspace root
num_parent_dirs = 2 + source_file_name.count(os.path.sep)
relative_prefix = "".join(["../"] * num_parent_dirs)
symlink_target = relative_prefix + source
os.symlink(symlink_target, symlink_location)
relative_source = source_file_name
combined_lines.append(" \"%s\"," % relative_source)
else:
print("Source {} is not inside module path {}".format(source, module["path"]))
sys.exit(1)
else:
relative_source = source[len(module["path"]) + 1:]
combined_lines.append(" \"%s\"," % relative_source)
combined_lines.append(" ],")
if module_type == "objc_library" or module_type == "cc_library":
if len(module["includes"]) == 0:
combined_lines.append(" publicHeadersPath: \"\",")
elif len(module["includes"]) == 1:
combined_lines.append(" publicHeadersPath: \"%s\"," % module["includes"][0])
else:
print("Multiple includes are not supported yet: {}".format(module["includes"]))
sys.exit(1)
combined_lines.append(" cSettings: [")
combined_lines.append(" .unsafeFlags([")
for flag in module["copts"]:
# Escape C-string entities in flag
escaped_flag = flag.replace('\\', '\\\\').replace('"', '\\"')
combined_lines.append(" \"%s\"," % escaped_flag)
combined_lines.append(" ])")
combined_lines.append(" ],")
combined_lines.append(" linkerSettings: [")
if module_type == "objc_library":
for framework in module["sdk_frameworks"]:
combined_lines.append(" .linkedFramework(\"%s\")," % framework)
for dylib in module["sdk_dylibs"]:
combined_lines.append(" .linkedLibrary(\"%s\")," % dylib)
combined_lines.append(" ]")
elif module_type == "swift_library":
combined_lines.append(" swiftSettings: [")
combined_lines.append(" .swiftLanguageMode(.v5),")
combined_lines.append(" .unsafeFlags([")
for flag in module["copts"]:
combined_lines.append(" \"%s\"," % flag)
combined_lines.append(" ])")
combined_lines.append(" ]")
combined_lines.append(" ),")
elif module["type"] == "root":
pass
else:
print("Unknown module type: {}".format(module["type"]))
sys.exit(1)
combined_lines.append(" ]")
combined_lines.append(")")
combined_lines.append("")
with open("Package.swift", "w") as f:
f.write("\n".join(combined_lines))

View File

@ -322,6 +322,7 @@ public enum ResolvedUrl {
case premiumMultiGift(reference: String?)
case collectible(gift: StarGift.UniqueGift?)
case messageLink(link: TelegramResolvedMessageLink?)
case stars
}
public enum ResolveUrlResult {

View File

@ -141,6 +141,7 @@ public enum StarsPurchasePurpose: Equatable {
case upgradeStarGift(requiredStars: Int64)
case transferStarGift(requiredStars: Int64)
case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64)
case buyStarGift(requiredStars: Int64)
}
public struct PremiumConfiguration {

View File

@ -611,7 +611,7 @@ private final class ActionButtonPanelNode: ASDisplayNode {
private(set) var isAccepted: Bool = false
var isAcceptedUpdated: (() -> Void)?
var openRecurrentTerms: (() -> Void)?
private var recurrentConfirmationNode: RecurrentConfirmationNode?
var recurrentConfirmationNode: RecurrentConfirmationNode?
func update(presentationData: PresentationData, layout: ContainerViewLayout, invoice: BotPaymentInvoice?, botName: String?) -> (CGFloat, CGFloat) {
let bottomPanelVerticalInset: CGFloat = 16.0
@ -1211,7 +1211,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
payString = self.presentationData.strings.CheckoutInfo_Pay
}
self.actionButton.isEnabled = isButtonEnabled
self.actionButton.isEnabled = true
self.actionButton.isImplicitlyDisabled = !isButtonEnabled
if let currentPaymentMethod = self.currentPaymentMethod {
switch currentPaymentMethod {
@ -1268,7 +1269,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}
@objc func actionButtonPressed() {
self.pay()
if let recurrentConfirmationNode = self.actionButtonPanelNode.recurrentConfirmationNode, !self.actionButtonPanelNode.isAccepted {
recurrentConfirmationNode.layer.addShakeAnimation()
} else {
self.pay()
}
}
private func pay(savedCredentialsToken: TemporaryTwoStepPasswordToken? = nil, liabilityNoticeAccepted: Bool = false, receivedCredentials: BotPaymentCredentials? = nil) {

View File

@ -16,6 +16,7 @@ import TelegramBaseController
import InviteLinksUI
import UndoUI
import TelegramCallsUI
import TelegramUIPreferences
public enum CallListControllerMode {
case tab
@ -734,10 +735,22 @@ public final class CallListController: TelegramBaseController {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let strongSelf = self else {
guard let self else {
return
}
strongSelf.callPressed()
self.callPressed()
})
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_HideCallsTab, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HideIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let _ = updateCallListSettingsInteractively(accountManager: self.context.sharedContext.accountManager, {
$0.withUpdatedShowTab(false)
}).start()
})
})))

View File

@ -1015,7 +1015,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0 ? true : false)
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0)
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
if let scrollingOffset = self.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition)

View File

@ -2092,8 +2092,6 @@ public final class ChatListNode: ListView {
return .single(.setupPhoto(accountPeer))
} else if suggestions.contains(.gracePremium) {
return .single(.premiumGrace)
} else if suggestions.contains(.setupBirthday) && birthday == nil {
return .single(.setupBirthday)
} else if suggestions.contains(.xmasPremiumGift) {
return .single(.xmasPremiumGift)
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
@ -2149,6 +2147,8 @@ public final class ChatListNode: ListView {
}
return .birthdayPremiumGift(peers: todayBirthdayPeers, birthdays: birthdays)
}
} else if suggestions.contains(.setupBirthday) && birthday == nil {
return .single(.setupBirthday)
} else if case let .link(id, url, title, subtitle) = suggestions.first(where: { if case .link = $0 { return true } else { return false} }) {
return .single(.link(id: id, url: url, title: title, subtitle: subtitle))
} else {

View File

@ -501,7 +501,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
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
fixedTransitionDirection = isExtracted
}
} else {
animationFraction = 1.0

View File

@ -1106,6 +1106,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
if let rightLabelTextLayoutAndApply {
additionalTitleInset += rightLabelTextLayoutAndApply.0.size.width + 36.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var maxStatusWidth: CGFloat = params.width - leftInset - rightInset - badgeSize

View File

@ -527,7 +527,12 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
guard let strongSelf = self, let _ = gesture else {
return
}
let localPoint = strongSelf.view.convert(point, from: view)
let localPoint: CGPoint
if let layout = strongSelf.validLayout, layout.metrics.isTablet, layout.size.width > layout.size.height, let view {
localPoint = view.convert(point, to: nil)
} else {
localPoint = strongSelf.view.convert(point, from: view)
}
let initialPoint: CGPoint
if let current = strongSelf.initialContinueGesturePoint {
initialPoint = current

View File

@ -167,10 +167,12 @@ public func searchCountries(items: [((String, String), String, [Int])], query: S
let componentsOne = item.0.0.components(separatedBy: " ")
let abbrOne = componentsOne.compactMap { $0.first.flatMap { String($0) } }.reduce(into: String(), { $0.append(contentsOf: $1) }).replacingOccurrences(of: "&", with: "")
let componentsTwo = item.0.0.components(separatedBy: " ")
let componentsTwo = item.0.1.components(separatedBy: " ")
let abbrTwo = componentsTwo.compactMap { $0.first.flatMap { String($0) } }.reduce(into: String(), { $0.append(contentsOf: $1) }).replacingOccurrences(of: "&", with: "")
let string = "\(item.0.0) \((item.0.1)) \(item.1) \(abbrOne) \(abbrTwo)"
let phoneCodes = item.2.map { "\($0)" }.joined(separator: " ")
let string = "\(item.0.0) \((item.0.1)) \(item.1) \(abbrOne) \(abbrTwo) \(phoneCodes)"
let tokens = stringTokens(string)
if matchStringTokens(tokens, with: queryTokens) {
for code in item.2 {

View File

@ -697,7 +697,7 @@ final class ColorGridComponent: Component {
bottomRightRadius = largeCornerRadius
}
let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5 ? true : false
let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5
var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, isLight: isLight)
if selectionKnobImage != self.selectionKnobImage {

View File

@ -713,7 +713,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
return .single(message.flatMap { ($0, false) })
}
}
translateToLanguage = chatTranslationState(context: context, peerId: messageId.peerId)
translateToLanguage = chatTranslationState(context: context, peerId: messageId.peerId, threadId: threadIdValue)
|> map { translationState in
if let translationState, translationState.isEnabled {
let translateToLanguage = translationState.toLang ?? baseLanguageCode

@ -1 +1 @@
Subproject commit b885e63e766890d1cbf36b66cfe27cca55a6ec90
Subproject commit 4a3144b5d527429f7bbd0f07003cb372bf8939ce

View File

@ -53,15 +53,29 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
}
if let value = attributes[.font], let font = value as? UIFont {
let fontName = font.fontName.lowercased()
if fontName.contains("bolditalic") {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
} else if fontName.contains("bold") {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
} else if fontName.contains("italic") {
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
} else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") {
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
if fontName.hasPrefix(".sfui") {
let traits = font.fontDescriptor.symbolicTraits
if traits.contains(.traitMonoSpace) {
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
} else {
if traits.contains(.traitBold) {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
}
if traits.contains(.traitItalic) {
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
}
}
} else {
if fontName.contains("bolditalic") {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
} else if fontName.contains("bold") {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
} else if fontName.contains("italic") {
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
} else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") {
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
}
}
}
if let value = attributes[.backgroundColor] as? UIColor, value.rgb == UIColor.gray.rgb {

View File

@ -1124,7 +1124,8 @@ private final class LimitSheetContent: CombinedComponent {
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
titleText = strings.Premium_CreateMultipleStories
if isPremiumDisabled {
badgeText = "\(limit)"
let numberString = strings.Premium_MaxExpiringStoriesNoPremiumTextNumberFormat(Int32(limit))

View File

@ -174,6 +174,10 @@ public final class ShimmerEffectForegroundView: UIView {
}
}
private let shadowImage: UIImage? = {
UIImage(named: "Stories/PanelGradient")
}()
public final class ShimmerEffectForegroundNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
@ -232,23 +236,32 @@ public final class ShimmerEffectForegroundNode: ASDisplayNode {
let image: UIImage?
if horizontal {
image = generateImage(CGSize(width: effectSize ?? 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
let baseAlpha: CGFloat = 0.1
image = generateImage(CGSize(width: effectSize ?? 200.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
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()
}
})
} else {
image = generateImage(CGSize(width: 16.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in

View File

@ -166,6 +166,11 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
self.conferenceAddParticipant?()
}
var enableVideoSharpening = false
if let data = call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_call_video_sharpening"] as? Double {
enableVideoSharpening = value != 0.0
}
self.callScreenState = PrivateCallScreen.State(
strings: presentationData.strings,
@ -180,7 +185,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
remoteVideo: nil,
isRemoteBatteryLow: false,
isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency,
isConferencePossible: false
isConferencePossible: false,
enableVideoSharpening: enableVideoSharpening
)
self.isMicrophoneMutedDisposable = (call.isMuted

View File

@ -1160,7 +1160,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
useIPCContext = value != 0.0
}
let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type"
let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type-v2"
let screencastIPCContext: ScreencastIPCContext
if useIPCContext {

View File

@ -7,6 +7,7 @@ import BalancedTextComponent
import TelegramPresentationData
import CallsEmoji
import ImageBlur
import HierarchyTrackingLayer
private final class EmojiContainerView: UIView {
private let maskImageView: UIImageView?
@ -207,6 +208,7 @@ private final class EmojiItemComponent: Component {
}
final class View: UIView {
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private let containerView: EmojiContainerView
private let measureEmojiView = ComponentView<Empty>()
private var pendingContainerView: EmojiContainerView?
@ -219,11 +221,22 @@ private final class EmojiItemComponent: Component {
private var pendingEmojiValues: [String]?
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
self.containerView = EmojiContainerView(hasMask: true)
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.addSubview(self.containerView)
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
guard let self else {
return
}
if value {
self.state?.updated(transition: .immediate)
}
}
}
required init?(coder: NSCoder) {

View File

@ -275,7 +275,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
if let current = self.videoLayer {
videoLayer = current
} else {
videoLayer = PrivateCallVideoLayer()
videoLayer = PrivateCallVideoLayer(enableSharpening: false)
self.videoLayer = videoLayer
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)

View File

@ -51,6 +51,7 @@ final class VideoChatParticipantVideoComponent: Component {
let contentInsets: UIEdgeInsets
let controlInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
let enableVideoSharpening: Bool
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let activatePinch: ((PinchSourceContainerNode) -> Void)?
@ -70,6 +71,7 @@ final class VideoChatParticipantVideoComponent: Component {
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
enableVideoSharpening: Bool,
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?,
activatePinch: ((PinchSourceContainerNode) -> Void)?,
@ -88,6 +90,7 @@ final class VideoChatParticipantVideoComponent: Component {
self.contentInsets = contentInsets
self.controlInsets = controlInsets
self.interfaceOrientation = interfaceOrientation
self.enableVideoSharpening = enableVideoSharpening
self.action = action
self.contextAction = contextAction
self.activatePinch = activatePinch
@ -128,6 +131,9 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if lhs.enableVideoSharpening != rhs.enableVideoSharpening {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
@ -525,7 +531,7 @@ final class VideoChatParticipantVideoComponent: Component {
resetVideoSource = true
}
} else {
videoLayer = PrivateCallVideoLayer()
videoLayer = PrivateCallVideoLayer(enableSharpening: component.enableVideoSharpening)
self.videoLayer = videoLayer
videoLayer.opacity = 0.0
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)

View File

@ -152,6 +152,7 @@ final class VideoChatParticipantsComponent: Component {
let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
let enableVideoSharpening: Bool
let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
let openInvitedParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
@ -173,6 +174,7 @@ final class VideoChatParticipantsComponent: Component {
expandedInsets: UIEdgeInsets,
safeInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
enableVideoSharpening: Bool,
openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
openInvitedParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
@ -193,6 +195,7 @@ final class VideoChatParticipantsComponent: Component {
self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
self.interfaceOrientation = interfaceOrientation
self.enableVideoSharpening = enableVideoSharpening
self.openParticipantContextMenu = openParticipantContextMenu
self.openInvitedParticipantContextMenu = openInvitedParticipantContextMenu
self.updateMainParticipant = updateMainParticipant
@ -239,6 +242,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if lhs.enableVideoSharpening != rhs.enableVideoSharpening {
return false
}
return true
}
@ -1074,6 +1080,7 @@ final class VideoChatParticipantsComponent: Component {
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
interfaceOrientation: component.interfaceOrientation,
enableVideoSharpening: component.enableVideoSharpening,
action: { [weak self] in
guard let self, let component = self.component else {
return

View File

@ -234,6 +234,8 @@ final class VideoChatScreenComponent: Component {
let participants = ComponentView<Empty>()
var scheduleInfo: ComponentView<Empty>?
var enableVideoSharpening: Bool = false
var reconnectedAsEventsDisposable: Disposable?
var memberEventsDisposable: Disposable?
@ -1244,6 +1246,11 @@ final class VideoChatScreenComponent: Component {
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.id == .peer(invitedPeer.peer.id) }) })
}
self.callState = component.initialData.callState
self.enableVideoSharpening = false
if let data = component.initialCall.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_call_video_sharpening"] as? Double {
self.enableVideoSharpening = value != 0.0
}
}
var call: VideoChatCall
@ -1359,7 +1366,7 @@ final class VideoChatScreenComponent: Component {
return false
}
if participant.videoDescription != nil || participant.presentationDescription != nil {
if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) {
if let participantPeer = participant.peer, participantPeer.id != groupCall.accountContext.account.peerId, members.speakingParticipants.contains(participantPeer.id) {
return true
}
}
@ -1421,7 +1428,7 @@ final class VideoChatScreenComponent: Component {
var speakingParticipantPeers: [EnginePeer] = []
if let members, !members.speakingParticipants.isEmpty {
for participant in members.participants {
if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) {
if let participantPeer = participant.peer, participantPeer.id != groupCall.accountContext.account.peerId, members.speakingParticipants.contains(participantPeer.id) {
speakingParticipantPeers.append(participantPeer)
}
}
@ -1698,7 +1705,7 @@ final class VideoChatScreenComponent: Component {
return false
}
if participant.videoDescription != nil || participant.presentationDescription != nil {
if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) {
if let participantPeer = participant.peer, participantPeer.id != conferenceSource.context.account.peerId, members.speakingParticipants.contains(participantPeer.id) {
return true
}
}
@ -1760,7 +1767,7 @@ final class VideoChatScreenComponent: Component {
var speakingParticipantPeers: [EnginePeer] = []
if !members.speakingParticipants.isEmpty {
for participant in members.participants {
if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) {
if let participantPeer = participant.peer, participantPeer.id != conferenceSource.context.account.peerId, members.speakingParticipants.contains(participantPeer.id) {
speakingParticipantPeers.append(participantPeer)
}
}
@ -2501,6 +2508,7 @@ final class VideoChatScreenComponent: Component {
expandedInsets: participantsExpandedInsets,
safeInsets: participantsSafeInsets,
interfaceOrientation: environment.orientation ?? .portrait,
enableVideoSharpening: self.enableVideoSharpening,
openParticipantContextMenu: { [weak self] id, sourceView, gesture in
guard let self else {
return

View File

@ -469,6 +469,10 @@ final class PendingStoryManager {
}
})
} else {
if let uploadInfo = pendingItemContext.item.uploadInfo {
let partTotalProgress = 1.0 / Float(uploadInfo.total)
pendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress
}
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo)
|> deliverOn(self.queue)).start(next: { [weak self] event in
guard let `self` = self else {

View File

@ -1542,24 +1542,7 @@ private final class ProfileGiftsContextImpl {
return EmptyDisposable
}
var saveToProfile = false
if let gift = self.gifts.first(where: { $0.reference == reference }) {
if !gift.savedToProfile {
saveToProfile = true
}
} else if let gift = self.filteredGifts.first(where: { $0.reference == reference }) {
if !gift.savedToProfile {
saveToProfile = true
}
}
var signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
if saveToProfile {
signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)
|> castError(UpdateStarGiftPriceError.self)
|> then(signal)
}
let signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
let disposable = MetaDisposable()
disposable.set(
(signal
@ -1584,7 +1567,7 @@ private final class ProfileGiftsContextImpl {
}) {
if case let .unique(uniqueGift) = self.gifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift))
self.gifts[index] = updatedGift
}
}
@ -1607,7 +1590,7 @@ private final class ProfileGiftsContextImpl {
}) {
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift))
self.filteredGifts[index] = updatedGift
}
}
@ -2438,6 +2421,7 @@ private final class ResaleGiftsContextImpl {
private var gifts: [StarGift] = []
private var attributes: [StarGift.UniqueGift.Attribute] = []
private var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
private var attributesHash: Int64?
private var count: Int32?
private var dataState: ResaleGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil)
@ -2477,6 +2461,7 @@ private final class ResaleGiftsContextImpl {
let postbox = self.account.postbox
let sorting = self.sorting
let filterAttributes = self.filterAttributes
let currentAttributesHash = self.attributesHash
let dataState = self.dataState
@ -2511,46 +2496,45 @@ private final class ResaleGiftsContextImpl {
}
}
var attributesHash: Int64?
if "".isEmpty {
flags |= (1 << 0)
attributesHash = 0
}
let attributesHash = currentAttributesHash ?? 0
flags |= (1 << 0)
let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.ResaleStarGifts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?), NoError> in
|> mapToSignal { result -> Signal<([StarGift], [StarGift.UniqueGift.Attribute]?, [ResaleGiftsContext.Attribute: Int32]?, Int64?, Int32, String?), NoError> in
guard let result else {
return .single(([], [], [:], 0, nil))
return .single(([], nil, nil, nil, 0, nil))
}
return postbox.transaction { transaction -> ([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?) in
return postbox.transaction { transaction -> ([StarGift], [StarGift.UniqueGift.Attribute]?, [ResaleGiftsContext.Attribute: Int32]?, Int64?, Int32, String?) in
switch result {
case let .resaleStarGifts(_, count, gifts, nextOffset, attributes, attributesHash, chats, counters, users):
let _ = attributesHash
var resultAttributes: [StarGift.UniqueGift.Attribute] = []
var resultAttributes: [StarGift.UniqueGift.Attribute]?
if let attributes {
resultAttributes = attributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) }
}
var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
var attributeCount: [ResaleGiftsContext.Attribute: Int32]?
if let counters {
var attributeCountValue: [ResaleGiftsContext.Attribute: Int32] = [:]
for counter in counters {
switch counter {
case let .starGiftAttributeCounter(attribute, count):
switch attribute {
case let .starGiftAttributeIdModel(documentId):
attributeCount[.model(documentId)] = count
attributeCountValue[.model(documentId)] = count
case let .starGiftAttributeIdPattern(documentId):
attributeCount[.pattern(documentId)] = count
attributeCountValue[.pattern(documentId)] = count
case let .starGiftAttributeIdBackdrop(backdropId):
attributeCount[.backdrop(backdropId)] = count
attributeCountValue[.backdrop(backdropId)] = count
}
}
}
attributeCount = attributeCountValue
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
@ -2563,13 +2547,13 @@ private final class ResaleGiftsContextImpl {
}
}
return (mappedGifts, resultAttributes, attributeCount, count, nextOffset)
return (mappedGifts, resultAttributes, attributeCount, attributesHash, count, nextOffset)
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] (gifts, attributes, attributeCount, count, nextOffset) in
|> deliverOn(self.queue)).start(next: { [weak self] (gifts, attributes, attributeCount, attributesHash, count, nextOffset) in
guard let self else {
return
}
@ -2581,10 +2565,13 @@ private final class ResaleGiftsContextImpl {
let updatedCount = max(Int32(self.gifts.count), count)
self.count = updatedCount
self.attributes = attributes
if !attributeCount.isEmpty {
if let attributes, let attributeCount, let attributesHash {
self.attributes = attributes
self.attributeCount = attributeCount
self.attributesHash = attributesHash
}
self.dataState = .ready(canLoadMore: count != 0 && updatedCount > self.gifts.count && nextOffset != nil, nextOffset: nextOffset)
self.pushState()

View File

@ -142,7 +142,7 @@ public extension Peer {
var largeProfileImage: TelegramMediaImageRepresentation? {
return largestImageRepresentation(self.profileImageRepresentations)
}
var isDeleted: Bool {
switch self {
case let user as TelegramUser:
@ -152,6 +152,27 @@ public extension Peer {
}
}
var isGenericUser: Bool {
switch self {
case let user as TelegramUser:
if user.isDeleted {
return false
}
if user.botInfo != nil {
return false
}
if user.id.isRepliesOrVerificationCodes {
return false
}
if user.id.isTelegramNotifications {
return false
}
return true
default:
return false
}
}
var isScam: Bool {
switch self {
case let user as TelegramUser:

View File

@ -203,6 +203,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case channelSendGiftTooltip = 76
case starGiftWearTips = 77
case channelSuggestTooltip = 78
case multipleStoriesTooltip = 79
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -564,6 +565,10 @@ private struct ApplicationSpecificNoticeKeys {
static func channelSuggestTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.channelSuggestTooltip.key)
}
static func multipleStoriesTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleStoriesTooltip.key)
}
}
public struct ApplicationSpecificNotice {
@ -2426,4 +2431,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue)
}
}
public static func getMultipleStoriesTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip())?.get(ApplicationSpecificCounterNotice.self) {
return value.value
} else {
return 0
}
}
}
public static func incrementMultipleStoriesTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
return accountManager.transaction { transaction -> Int in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += Int32(count)
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip(), entry)
}
return Int(previousValue)
}
}
}

View File

@ -70,6 +70,7 @@ swift_library(
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramVoip:TelegramVoip",
"//submodules/DeviceAccess:DeviceAccess",
"//submodules/Utils/DeviceModel",
"//submodules/WatchCommon/Host:WatchCommon",
"//submodules/BuildConfig:BuildConfig",
"//submodules/BuildConfigExtra:BuildConfigExtra",

View File

@ -1331,11 +1331,7 @@ final class AvatarEditorScreenComponent: Component {
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
if isLocked {
self?.presentPremiumToast()
} else {
self?.complete()
}
self?.complete()
}
)
),
@ -1389,11 +1385,34 @@ final class AvatarEditorScreenComponent: Component {
parentController.present(controller, in: .window(.root))
}
private func isPremiumRequired() -> Bool {
guard let component = self.component, let state = self.state else {
return false
}
if component.peerType != .suggest, !component.context.isPremium {
if state.selectedBackground.isPremium {
return true
}
if let selectedFile = state.selectedFile {
if selectedFile.isSticker {
return true
}
}
}
return false
}
private let queue = Queue()
func complete() {
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
return
}
if self.isPremiumRequired() {
self.presentPremiumToast()
return
}
let context = controller.context
let _ = context.animationCache.getFirstFrame(queue: self.queue, sourceId: file.resource.id.stringRepresentation, size: CGSize(width: 640.0, height: 640.0), fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: nil), completion: { result in
guard let item = result.item else {

View File

@ -5,6 +5,21 @@ import MetalPerformanceShaders
import Accelerate
import MetalEngine
private func makeSharpenKernel(device: MTLDevice, sharpeningStrength: Float) -> MPSImageConvolution {
let centerWeight = 1.0 + 6.0 * sharpeningStrength
let adjacentWeight = -1.0 * sharpeningStrength
let diagonalWeight = -0.5 * sharpeningStrength
let sharpenWeights: [Float] = [
diagonalWeight, adjacentWeight, diagonalWeight,
adjacentWeight, centerWeight, adjacentWeight,
diagonalWeight, adjacentWeight, diagonalWeight
]
let result = MPSImageConvolution(device: device, kernelWidth: 3, kernelHeight: 3, weights: sharpenWeights)
result.edgeMode = .clamp
return result
}
public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
public var internalData: MetalEngineSubjectInternalData?
@ -16,6 +31,9 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
let computePipelineStateHorizontal: MTLComputePipelineState
let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale
var sharpeningStrength: Float = 0.0
var sharpenKernel: MPSImageConvolution
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
@ -52,6 +70,14 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
self.computePipelineStateVertical = computePipelineStateVertical
self.downscaleKernel = MPSImageBilinearScale(device: device)
self.sharpeningStrength = 1.4
self.sharpenKernel = makeSharpenKernel(device: device, sharpeningStrength: self.sharpeningStrength)
}
func updateSharpeningStrength(device: MTLDevice, sharpeningStrength: Float) {
self.sharpeningStrength = sharpeningStrength
self.sharpenKernel = makeSharpenKernel(device: device, sharpeningStrength: self.sharpeningStrength)
}
}
@ -82,21 +108,26 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
self.setNeedsUpdate()
}
}
private let enableSharpening: Bool
public var renderSpec: RenderLayerSpec?
private var rgbaTexture: PooledTexture?
private var sharpenedTexture: PooledTexture?
private var downscaledTexture: PooledTexture?
private var blurredHorizontalTexture: PooledTexture?
private var blurredVerticalTexture: PooledTexture?
override public init() {
public init(enableSharpening: Bool) {
self.enableSharpening = enableSharpening
self.blurredLayer = MetalEngineSubjectLayer()
super.init()
}
override public init(layer: Any) {
self.enableSharpening = false
self.blurredLayer = MetalEngineSubjectLayer()
super.init(layer: layer)
@ -121,6 +152,9 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
}
if self.sharpenedTexture == nil || self.sharpenedTexture?.spec != rgbaTextureSpec {
self.sharpenedTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
}
if self.downscaledTexture == nil {
self.downscaledTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
@ -134,35 +168,90 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {
return
}
var outputTexture = rgbaTexture
var sharpenedTexture: TexturePlaceholder?
if self.enableSharpening && rgbaTextureSpec.width * rgbaTextureSpec.height >= 800 * 480 {
sharpenedTexture = self.sharpenedTexture?.get(context: context)
if let sharpenedTexture {
outputTexture = sharpenedTexture
}
}
let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture in
guard let rgbaTexture else {
return
}
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
switch videoTextures.textureLayout {
case let .biPlanar(biPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
computeEncoder.setTexture(triPlanar.y, index: 0)
computeEncoder.setTexture(triPlanar.u, index: 1)
computeEncoder.setTexture(triPlanar.u, index: 2)
computeEncoder.setTexture(rgbaTexture, index: 3)
}
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
})
if let sharpenedTexture {
let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, sharpenedTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture, sharpenedTexture in
guard let rgbaTexture else {
return
}
guard let sharpenedTexture else {
return
}
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
switch videoTextures.textureLayout {
case let .biPlanar(biPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
computeEncoder.setTexture(triPlanar.y, index: 0)
computeEncoder.setTexture(triPlanar.u, index: 1)
computeEncoder.setTexture(triPlanar.u, index: 2)
computeEncoder.setTexture(rgbaTexture, index: 3)
}
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
do {
blurState.sharpenKernel.encode(commandBuffer: commandBuffer, sourceTexture: rgbaTexture, destinationTexture: sharpenedTexture)
}
})
} else {
let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture in
guard let rgbaTexture else {
return
}
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
switch videoTextures.textureLayout {
case let .biPlanar(biPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
computeEncoder.setTexture(triPlanar.y, index: 0)
computeEncoder.setTexture(triPlanar.u, index: 1)
computeEncoder.setTexture(triPlanar.u, index: 2)
computeEncoder.setTexture(rgbaTexture, index: 3)
}
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
})
}
if !self.blurredLayer.isHidden {
guard let downscaledTexture = self.downscaledTexture?.get(context: context), let blurredHorizontalTexture = self.blurredHorizontalTexture?.get(context: context), let blurredVerticalTexture = self.blurredVerticalTexture?.get(context: context) else {
@ -228,8 +317,8 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
})
}
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: rgbaTexture.placeholer, commands: { encoder, placement, rgbaTexture in
guard let rgbaTexture else {
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: outputTexture.placeholer, commands: { encoder, placement, outputTexture in
guard let outputTexture else {
return
}
@ -244,7 +333,7 @@ public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSu
)
encoder.setVertexBytes(&mirror, length: 2 * 4, index: 1)
encoder.setFragmentTexture(rgbaTexture, index: 0)
encoder.setFragmentTexture(outputTexture, index: 0)
var brightness: Float = 1.0
var saturation: Float = 1.0

View File

@ -128,6 +128,7 @@ final class VideoContainerView: HighlightTrackingButton {
}
let key: Key
let enableSharpening: Bool
let videoContainerLayer: VideoContainerLayer
var videoContainerLayerTaken: Bool = false
@ -211,8 +212,9 @@ final class VideoContainerView: HighlightTrackingButton {
var pressAction: (() -> Void)?
init(key: Key) {
init(key: Key, enableSharpening: Bool) {
self.key = key
self.enableSharpening = enableSharpening
self.videoContainerLayer = VideoContainerLayer()
self.videoContainerLayer.backgroundColor = nil
@ -223,7 +225,7 @@ final class VideoContainerView: HighlightTrackingButton {
self.videoContainerLayer.contentsLayer.cornerCurve = .circular
}
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer = PrivateCallVideoLayer(enableSharpening: self.enableSharpening)
self.videoLayer.masksToBounds = true
self.videoLayer.isDoubleSided = false
if #available(iOS 13.0, *) {
@ -454,7 +456,7 @@ final class VideoContainerView: HighlightTrackingButton {
let previousVideoLayer = self.videoLayer
self.disappearingVideoLayer = DisappearingVideo(flipAnimationInfo: flipAnimationInfo, videoLayer: self.videoLayer, videoMetrics: videoMetrics)
self.videoLayer = PrivateCallVideoLayer()
self.videoLayer = PrivateCallVideoLayer(enableSharpening: self.enableSharpening)
self.videoLayer.opacity = previousVideoLayer.opacity
self.videoLayer.masksToBounds = true
self.videoLayer.isDoubleSided = false

View File

@ -81,6 +81,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
public var isRemoteBatteryLow: Bool
public var isEnergySavingEnabled: Bool
public var isConferencePossible: Bool
public var enableVideoSharpening: Bool
public init(
strings: PresentationStrings,
@ -95,7 +96,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
remoteVideo: VideoSource?,
isRemoteBatteryLow: Bool,
isEnergySavingEnabled: Bool,
isConferencePossible: Bool
isConferencePossible: Bool,
enableVideoSharpening: Bool
) {
self.strings = strings
self.lifecycleState = lifecycleState
@ -110,6 +112,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.isRemoteBatteryLow = isRemoteBatteryLow
self.isEnergySavingEnabled = isEnergySavingEnabled
self.isConferencePossible = isConferencePossible
self.enableVideoSharpening = enableVideoSharpening
}
public static func ==(lhs: State, rhs: State) -> Bool {
@ -152,6 +155,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
if lhs.isConferencePossible != rhs.isConferencePossible {
return false
}
if lhs.enableVideoSharpening != rhs.enableVideoSharpening {
return false
}
return true
}
}
@ -994,7 +1000,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
videoContainerView = current
} else {
animateIn = true
videoContainerView = VideoContainerView(key: videoContainerKey)
videoContainerView = VideoContainerView(key: videoContainerKey, enableSharpening: params.state.enableVideoSharpening)
switch videoContainerKey {
case .foreground:
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)

View File

@ -3630,7 +3630,13 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.node.resumeCameraCapture(fromGallery: true)
}
var dismissControllerImpl: (() -> Void)?
class DismissArgs {
var resumeOnDismiss = true
}
var dismissControllerImpl: ((Bool) -> Void)?
let dismissArgs = DismissArgs()
let controller: ViewController
if let current = self.galleryController {
controller = current
@ -3686,7 +3692,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
}
dismissControllerImpl?()
dismissControllerImpl?(true)
} else {
stopCameraCapture()
@ -3759,17 +3765,19 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.node.collage?.addResults(signals: results)
}
} else {
self.node.animateOutToEditor()
if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
})
}
}
self.galleryController = nil
dismissControllerImpl?()
dismissControllerImpl?(false)
}, dismissed: { [weak self] in
resumeCameraCapture()
if dismissArgs.resumeOnDismiss {
resumeCameraCapture()
}
if let self {
self.node.hasGallery = false
self.node.requestUpdateLayout(transition: .immediate)
@ -3780,7 +3788,8 @@ public class CameraScreenImpl: ViewController, CameraScreen {
)
self.galleryController = controller
dismissControllerImpl = { [weak controller] in
dismissControllerImpl = { [weak controller] resume in
dismissArgs.resumeOnDismiss = resume
controller?.dismiss(animated: true)
}
}

View File

@ -469,13 +469,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.giftButton.isHidden = false
self.helpButton.isHidden = true
//TODO:release
self.suggestedPostButton.isHidden = false
self.presentGiftOrSuggestTooltip()
} else if case .broadcast = peer.info {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = false
self.suggestedPostButton.isHidden = true
self.presentGiftOrSuggestTooltip()
} else if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
self.giftButton.isHidden = true

View File

@ -780,7 +780,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance && uniquePatternFile == nil ? 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 + 12.0 : 0.0), size: giftSize)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 12.0 : 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
var iconSize = CGSize(width: 160.0, height: 160.0)
@ -852,7 +852,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let _ = ribbonTextApply()
let _ = moreApply()
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - 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)

View File

@ -295,10 +295,6 @@ public func canAddMessageReactions(message: Message) -> Bool {
return true
}
}
} else if let story = media as? TelegramMediaStory {
if story.isMention {
return false
}
}
}
return true

View File

@ -325,6 +325,8 @@ public class ChatMessagePaymentAlertController: AlertController {
private let balance = ComponentView<Empty>()
private var didAppear = false
public init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?, showBalance: Bool = true) {
self.context = context
self.presentationData = presentationData
@ -361,6 +363,15 @@ public class ChatMessagePaymentAlertController: AlertController {
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didAppear {
self.didAppear = true
if !layout.metrics.isTablet && layout.size.width > layout.size.height {
Queue.mainQueue().after(0.1) {
self.view.window?.endEditing(true)
}
}
}
if let context = self.context, let _ = self.parentNavigationController, self.showBalance {
let insets = layout.insets(options: .statusBar)
let balanceSize = self.balance.update(

View File

@ -334,8 +334,8 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
strongSelf.buttonNode.isHidden = fromYou || isGroupOrChannel
strongSelf.buttonTitleNode.isHidden = fromYou || isGroupOrChannel
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize)
if let media, mediaUpdated {
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - imageSize.width) / 2.0), y: 13.0), size: imageSize)
if let media, mediaUpdated {
let boundingSize = imageSize
var imageSize = boundingSize
let updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
@ -440,7 +440,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
}
}
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize)
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - width) / 2.0), y: 0.0), size: backgroundSize)
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)

View File

@ -1441,6 +1441,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .messageLink:
break
case .stars:
break
}
}
}))

View File

@ -4,6 +4,7 @@ import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TextFormat
import TelegramPresentationData
@ -211,27 +212,46 @@ private final class QuickShareScreenComponent: Component {
self.state = state
if self.component == nil {
let peers = component.context.engine.peers.recentPeers()
|> take(1)
|> mapToSignal { recentPeers -> Signal<[EnginePeer], NoError> in
if case let .peers(peers) = recentPeers, !peers.isEmpty {
return .single(peers.map(EnginePeer.init))
} else {
return component.context.account.stateManager.postbox.tailChatListView(
groupId: .root,
count: 20,
summaryComponents: ChatListEntrySummaryComponents()
)
|> take(1)
|> map { view -> [EnginePeer] in
var peers: [EnginePeer] = []
for entry in view.0.entries.reversed() {
if case let .MessageEntry(entryData) = entry {
if let user = entryData.renderedPeer.chatMainPeer as? TelegramUser, user.isGenericUser && user.id != component.context.account.peerId && !user.id.isSecretChat {
peers.append(EnginePeer(user))
}
}
}
return peers
}
}
}
self.disposable = combineLatest(queue: Queue.mainQueue(),
component.context.engine.peers.recentPeers() |> take(1),
peers,
component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId))
).start(next: { [weak self] recentPeers, accountPeer in
).start(next: { [weak self] peers, accountPeer in
guard let self else {
return
}
var result: [EnginePeer] = []
switch recentPeers {
case let .peers(peers):
result = peers.map(EnginePeer.init)
case .disabled:
break
}
if !result.isEmpty, let accountPeer {
self.peers = Array([accountPeer] + result.prefix(4))
if !peers.isEmpty, let accountPeer {
self.peers = Array([accountPeer] + peers.prefix(4))
self.state?.updated()
component.ready.set(.single(true))
} else {
self.environment?.controller()?.dismiss()
}
component.ready.set(.single(true))
})
component.gesture.externalUpdated = { [weak self] view, point in

View File

@ -955,6 +955,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
titleTransition = .immediate
}
let statusSpacing: CGFloat = 3.0
let titleSideInset: CGFloat = 6.0
var titleFrame: CGRect
if size.height > 40.0 {
@ -966,7 +967,12 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - verifiedIconWidth - statusIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), insets: titleInsets, animated: titleTransition.isAnimated)
titleSize.width += credibilityIconWidth
titleSize.width += verifiedIconWidth
titleSize.width += statusIconWidth
if statusIconWidth > 0.0 {
titleSize.width += statusIconWidth
if credibilityIconWidth > 0.0 {
titleSize.width += statusSpacing
}
}
let activitySize = self.activityNode.updateLayout(CGSize(width: clearBounds.size.width - titleSideInset * 2.0, height: clearBounds.size.height), alignment: .center)
let titleInfoSpacing: CGFloat = 0.0
@ -1006,6 +1012,9 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize)
nextIconX -= titleCredibilitySize.width
if credibilityIconWidth > 0.0 {
nextIconX -= statusSpacing
}
self.titleStatusIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleStatusSize.width, y: floor((titleFrame.height - titleStatusSize.height) / 2.0)), size: titleStatusSize)
nextIconX -= titleStatusSize.width

View File

@ -559,7 +559,7 @@ public final class GiftItemComponent: Component {
let price: String
switch component.subject {
case let .premium(_, priceValue), let .starGift(_, priceValue):
if priceValue.containsEmoji {
if priceValue.contains("#") {
buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
if !component.isSoldOut {
starsColor = UIColor(rgb: 0xffbe27)
@ -867,10 +867,12 @@ public final class GiftItemComponent: Component {
}
)
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes))
if let range = labelText.string.range(of: "#") {
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("# \(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes))
let range = (labelText.string as NSString).range(of: "#")
if range.location != NSNotFound {
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: range)
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: range)
labelText.addAttribute(.kern, value: -1.5, range: NSRange(location: range.upperBound, length: 1))
}
let resellSize = self.reselLabel.update(
@ -1048,11 +1050,13 @@ private final class ButtonContentComponent: 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: "⭐️")
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: component.tinted)), 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))
attributedText.addAttribute(.font, value: Font.semibold(component.tinted ? 14.0 : 15.0), range: range)
attributedText.addAttribute(.baselineOffset, value: -3.0, range: range)
attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(location: range.upperBound + 1, length: attributedText.length - range.upperBound - 1))
attributedText.addAttribute(.kern, value: -1.5, range: NSRange(location: range.upperBound, length: 1))
}
let titleSize = self.title.update(

View File

@ -235,6 +235,8 @@ final class GiftOptionsScreenComponent: Component {
private var chevronImage: (UIImage, PresentationTheme)?
private var resaleConfiguration: StarsSubscriptionConfiguration?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -408,9 +410,14 @@ final class GiftOptionsScreenComponent: Component {
switch gift {
case let .generic(gift):
if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars {
subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+")
let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator)
if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxAmount || availability.resale == 1 {
subject = .starGift(gift: gift, price: "# \(priceString)")
} else {
subject = .starGift(gift: gift, price: "# \(priceString)+")
}
} else {
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
subject = .starGift(gift: gift, price: "# \(presentationStringsFormattedNumber(Int32(gift.price), environment.dateTimeFormat.groupingSeparator))")
}
case let .unique(gift):
subject = .uniqueGift(gift: gift, price: nil)
@ -458,9 +465,13 @@ final class GiftOptionsScreenComponent: Component {
mainController.push(giftController)
}
} else {
var forceUnique = false
if let disallowedGifts = self.state?.disallowedGifts, disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
forceUnique = true
var forceUnique: Bool?
if let disallowedGifts = self.state?.disallowedGifts {
if disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
forceUnique = true
} else if !disallowedGifts.contains(.limited) && disallowedGifts.contains(.unique) {
forceUnique = false
}
}
let giftController = GiftSetupScreen(
@ -769,6 +780,8 @@ final class GiftOptionsScreenComponent: Component {
self.optionsPromise.set(component.context.engine.payments.starsTopUpOptions()
|> map(Optional.init))
}
self.resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
}
self.component = component
@ -1554,6 +1567,9 @@ final class GiftOptionsScreenComponent: Component {
}
}
}
if disallowedGifts.contains(.unique) && gift.availability?.remains == 0 {
return false
}
}
return true
}

View File

@ -788,57 +788,60 @@ final class GiftSetupScreenComponent: Component {
contentHeight += sectionSpacing
}
if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability, availability.resale > 0 {
let resaleSectionSize = self.resaleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
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_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor))
if case let .starGift(starGift, forceUnique) = component.subject, let availability = starGift.availability, availability.resale > 0 {
if let forceUnique, !forceUnique {
} else {
let resaleSectionSize = self.resaleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
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_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor))
)
)),
], alignment: .left, spacing: 2.0)),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator),
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0
))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component, let controller = environment.controller() else {
return
}
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
peerId: component.peerId,
gift: starGift
)
)),
], alignment: .left, spacing: 2.0)),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator),
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0
))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component, let controller = environment.controller() else {
return
controller.push(storeController)
}
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
peerId: component.peerId,
gift: starGift
)
controller.push(storeController)
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize)
if let resaleSectionView = self.resaleSection.view {
if resaleSectionView.superview == nil {
self.scrollView.addSubview(resaleSectionView)
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize)
if let resaleSectionView = self.resaleSection.view {
if resaleSectionView.superview == nil {
self.scrollView.addSubview(resaleSectionView)
}
transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame)
}
transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame)
contentHeight += resaleSectionSize.height
contentHeight += sectionSpacing
}
contentHeight += resaleSectionSize.height
contentHeight += sectionSpacing
}
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
@ -1128,7 +1131,7 @@ final class GiftSetupScreenComponent: Component {
if isChannelGift {
upgradeFooterRawString = environment.strings.Gift_SendChannel_Upgrade_Info(peerName).string
} else {
if forceUnique {
if forceUnique == true {
upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_ForcedInfo(peerName).string
} else {
upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_Info(peerName).string
@ -1201,8 +1204,8 @@ final class GiftSetupScreenComponent: Component {
)
)),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.includeUpgrade, isEnabled: !forceUnique, action: { [weak self] _ in
guard let self, !forceUnique else {
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.includeUpgrade, isEnabled: forceUnique != true, action: { [weak self] _ in
guard let self, forceUnique != true else {
return
}
self.includeUpgrade = !self.includeUpgrade
@ -1748,7 +1751,7 @@ final class GiftSetupScreenComponent: Component {
public final class GiftSetupScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case premium(PremiumGiftProduct)
case starGift(StarGift.Gift, Bool)
case starGift(StarGift.Gift, Bool?)
}
private let context: AccountContext

View File

@ -109,6 +109,15 @@ public final class FilterSelectorComponent: Component {
return true
}
func animateIn() {
for (_, item) in self.visibleItems {
if let itemView = item.title.view {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state

View File

@ -71,7 +71,8 @@ private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presen
var title = "# \(name)"
var count = ""
if let counter = item.attributeCount[.model(file.fileId.id)] {
if let counter = item.attributeCount[attributeId] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(
@ -81,6 +82,7 @@ private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presen
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []

View File

@ -27,6 +27,8 @@ import UndoUI
import ContextUI
import LottieComponent
private let minimumCountToDisplayFilters = 18
final class GiftStoreScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -93,7 +95,8 @@ final class GiftStoreScreenComponent: Component {
private var starsStateDisposable: Disposable?
private var starsState: StarsContext.State?
private var initialCount: Int?
private var component: GiftStoreScreenComponent?
private(set) weak var state: State?
private var environment: EnvironmentType?
@ -148,6 +151,13 @@ final class GiftStoreScreenComponent: Component {
}
}
private var effectiveIsLoading: Bool {
if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading {
return true
}
return false
}
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else {
return
@ -163,6 +173,11 @@ final class GiftStoreScreenComponent: Component {
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
}
var topInset = environment.navigationHeight + 39.0
if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters {
topInset = environment.navigationHeight
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
if let starGifts = self.effectiveGifts {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
@ -172,7 +187,7 @@ final class GiftStoreScreenComponent: Component {
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 39.0 + 9.0), size: starsOptionSize)
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: starsOptionSize)
let controller = environment.controller
@ -215,7 +230,7 @@ final class GiftStoreScreenComponent: Component {
color: ribbonColor
)
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))")
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))")
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -240,9 +255,20 @@ final class GiftStoreScreenComponent: Component {
} else {
mainController = controller
}
let allSubjects: [GiftViewScreen.Subject] = (self.effectiveGifts ?? []).compactMap { gift in
if case let .unique(uniqueGift) = gift {
return .uniqueGift(uniqueGift, state.peerId)
}
return nil
}
let index = self.effectiveGifts?.firstIndex(where: { $0 == .unique(uniqueGift) }) ?? 0
let giftController = GiftViewScreen(
context: component.context,
subject: .uniqueGift(uniqueGift, state.peerId),
allSubjects: allSubjects,
index: index,
buyGift: { slug, peerId in
return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete()
},
@ -326,7 +352,6 @@ final class GiftStoreScreenComponent: Component {
showClearFilters = true
}
let topInset: CGFloat = environment.navigationHeight + 39.0
let bottomInset: CGFloat = environment.safeInsets.bottom
var emptyResultsActionFrame = CGRect(
@ -432,7 +457,7 @@ final class GiftStoreScreenComponent: Component {
}
func openSortContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -443,22 +468,31 @@ final class GiftStoreScreenComponent: Component {
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.value)
guard let self else {
return
}
self.state?.starGiftsContext.updateSorting(.value)
self.scrollToTop()
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.date)
guard let self else {
return
}
self.state?.starGiftsContext.updateSorting(.date)
self.scrollToTop()
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByNumber, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.number)
guard let self else {
return
}
self.state?.starGiftsContext.updateSorting(.number)
self.scrollToTop()
})))
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
@ -466,10 +500,10 @@ final class GiftStoreScreenComponent: Component {
}
func openModelContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
@ -531,6 +565,7 @@ final class GiftStoreScreenComponent: Component {
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
},
selectAll: { [weak self] in
guard let self else {
@ -543,6 +578,7 @@ final class GiftStoreScreenComponent: Component {
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
}
), false))
@ -557,7 +593,7 @@ final class GiftStoreScreenComponent: Component {
}
func openBackdropContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -622,6 +658,7 @@ final class GiftStoreScreenComponent: Component {
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
},
selectAll: { [weak self] in
guard let self else {
@ -634,6 +671,7 @@ final class GiftStoreScreenComponent: Component {
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
}
), false))
@ -648,7 +686,7 @@ final class GiftStoreScreenComponent: Component {
}
func openSymbolContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -713,6 +751,7 @@ final class GiftStoreScreenComponent: Component {
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
},
selectAll: { [weak self] in
guard let self else {
@ -725,6 +764,7 @@ final class GiftStoreScreenComponent: Component {
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
self.scrollToTop()
}
), false))
@ -763,6 +803,8 @@ final class GiftStoreScreenComponent: Component {
}
self.component = component
let isLoading = self.effectiveIsLoading
let theme = environment.theme
let strings = environment.strings
@ -777,7 +819,10 @@ final class GiftStoreScreenComponent: Component {
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let topPanelHeight = environment.navigationHeight + 39.0
var topPanelHeight = environment.navigationHeight + 39.0
if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters {
topPanelHeight = environment.navigationHeight
}
let topPanelSize = self.topPanel.update(
transition: transition,
@ -861,6 +906,10 @@ final class GiftStoreScreenComponent: Component {
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
}
var topInset: CGFloat = 0.0
if environment.statusBarHeight > 0.0 {
topInset = environment.statusBarHeight - 6.0
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
@ -874,11 +923,14 @@ final class GiftStoreScreenComponent: Component {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 10.0), size: titleSize))
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: topInset + 10.0), size: titleSize))
}
let effectiveCount: Int32
if let count = self.effectiveGifts?.count {
if let count = self.effectiveGifts?.count, count > 0 || self.initialCount != nil {
if self.initialCount == nil {
self.initialCount = count
}
effectiveCount = Int32(count)
} else if let resale = component.gift.availability?.resale {
effectiveCount = Int32(resale)
@ -889,14 +941,14 @@ final class GiftStoreScreenComponent: Component {
let subtitleSize = self.subtitle.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ForResale(effectiveCount), font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: effectiveCount == 0 ? environment.strings.Gift_Store_ForResaleNoResults : environment.strings.Gift_Store_ForResale(effectiveCount), font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 100.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) / 2.0), y: 31.0), size: subtitleSize)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) / 2.0), y: topInset + 31.0), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
@ -958,10 +1010,10 @@ final class GiftStoreScreenComponent: Component {
modelTitle = environment.strings.Gift_Store_Filter_Selected_Model(modelCount)
}
if backdropCount > 0 {
backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(modelCount)
backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(backdropCount)
}
if symbolCount > 0 {
symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(modelCount)
symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(symbolCount)
}
}
@ -993,13 +1045,15 @@ final class GiftStoreScreenComponent: Component {
}
))
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
let filterSize = self.filterSelector.update(
transition: transition,
component: AnyComponent(FilterSelectorComponent(
context: component.context,
colors: FilterSelectorComponent.Colors(
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15)
background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
),
items: filterItems
)),
@ -1008,9 +1062,14 @@ final class GiftStoreScreenComponent: Component {
)
if let filterSelectorView = self.filterSelector.view {
if filterSelectorView.superview == nil {
filterSelectorView.alpha = 0.0
self.addSubview(filterSelectorView)
}
transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: 56.0), size: filterSize))
transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: topInset + 56.0), size: filterSize))
if let initialCount = self.initialCount, initialCount >= minimumCountToDisplayFilters {
loadingTransition.setAlpha(view: filterSelectorView, alpha: 1.0)
}
}
if let starGifts = self.state?.starGiftsState?.gifts {
@ -1052,20 +1111,14 @@ final class GiftStoreScreenComponent: Component {
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
var isLoading = false
if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading {
isLoading = true
}
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
if isLoading {
self.loadingNode.update(size: availableSize, theme: environment.theme, transition: .immediate)
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0)
} else {
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
}
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: availableSize))
return availableSize
}
@ -1078,19 +1131,22 @@ final class GiftStoreScreenComponent: Component {
final class State: ComponentState {
private let context: AccountContext
var peerId: EnginePeer.Id
private let gift: StarGift.Gift
private var disposable: Disposable?
fileprivate let starGiftsContext: ResaleGiftsContext
fileprivate var starGiftsState: ResaleGiftsContext.State?
init(
context: AccountContext,
peerId: EnginePeer.Id,
giftId: Int64
gift: StarGift.Gift
) {
self.context = context
self.peerId = peerId
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId)
self.gift = gift
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id)
super.init()
@ -1110,7 +1166,7 @@ final class GiftStoreScreenComponent: Component {
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId, giftId: self.gift.id)
return State(context: self.context, peerId: self.peerId, gift: self.gift)
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {

View File

@ -154,10 +154,17 @@ final class LoadingShimmerNode: ASDisplayNode {
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let sideInset: CGFloat = 16.0
let filterSpacing: CGFloat = 6.0
let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0
for i in 0 ..< 4 {
context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0), size: CGSize(width: filterWidth, height: 28.0)), cornerWidth: 14.0, cornerHeight: 14.0, transform: nil))
}
var currentY: CGFloat = 39.0 + 7.0
var rowIndex: Int = 0
let sideInset: CGFloat = 16.0// + environment.safeInsets.left
let optionSpacing: CGFloat = 10.0
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let itemSize = CGSize(width: optionWidth, height: 154.0)
@ -167,7 +174,7 @@ final class LoadingShimmerNode: ASDisplayNode {
while currentY < size.height {
for i in 0 ..< 3 {
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 2.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 39.0 + 9.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
}
currentY += itemSize.height

View File

@ -0,0 +1,241 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import TelegramPresentationData
import ViewControllerComponent
import AccountContext
final class GiftPagerComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
public final class Item: Equatable {
let id: AnyHashable
let subject: GiftViewScreen.Subject
public init(id: AnyHashable, subject: GiftViewScreen.Subject) {
self.id = id
self.subject = subject
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
}
let context: AccountContext
let items: [Item]
let index: Int
let itemSpacing: CGFloat
let updated: (CGFloat, Int) -> Void
public init(
context: AccountContext,
items: [Item],
index: Int = 0,
itemSpacing: CGFloat = 0.0,
updated: @escaping (CGFloat, Int) -> Void
) {
self.context = context
self.items = items
self.index = index
self.itemSpacing = itemSpacing
self.updated = updated
}
public static func ==(lhs: GiftPagerComponent, rhs: GiftPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.itemSpacing != rhs.itemSpacing {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<EnvironmentType>] = [:]
private var component: GiftPagerComponent?
private var environment: Environment<EnvironmentType>?
override init(frame: CGRect) {
self.scrollView = UIScrollView(frame: frame)
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isSwiping: Bool = false
private var lastScrollTime: TimeInterval = 0
private let swipeInactiveThreshold: TimeInterval = 0.5
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.isSwiping = true
self.lastScrollTime = CACurrentMediaTime()
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.isSwiping = false
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.isSwiping = false
}
private var ignoreContentOffsetChange = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let component = self.component, let environment = self.environment, !self.ignoreContentOffsetChange && !self.isUpdating else {
return
}
if self.isSwiping {
self.lastScrollTime = CACurrentMediaTime()
}
self.ignoreContentOffsetChange = true
let _ = self.update(component: component, availableSize: self.bounds.size, environment: environment, transition: .immediate)
component.updated(self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width), component.items.count)
self.ignoreContentOffsetChange = false
}
private var isUpdating = true
func update(component: GiftPagerComponent, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
var validIds: [AnyHashable] = []
self.component = component
self.environment = environment
let firstTime = self.itemViews.isEmpty
let itemWidth = availableSize.width
let totalWidth = itemWidth * CGFloat(component.items.count) + component.itemSpacing * CGFloat(max(0, component.items.count - 1))
let contentSize = CGSize(width: totalWidth, height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollFrame = CGRect(origin: .zero, size: availableSize)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
let initialOffset = CGFloat(component.index) * (itemWidth + component.itemSpacing)
self.scrollView.contentOffset = CGPoint(x: initialOffset, y: 0.0)
var position: CGFloat
if self.scrollView.contentSize.width > self.scrollView.frame.width {
position = self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width)
} else {
position = 0.0
}
component.updated(position, component.items.count)
}
let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5
let currentTime = CACurrentMediaTime()
let isSwipingActive = self.isSwiping || (currentTime - self.lastScrollTime < self.swipeInactiveThreshold)
var i = 0
for item in component.items {
let itemOriginX = (itemWidth + component.itemSpacing) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: 0.0), size: CGSize(width: itemWidth, height: availableSize.height))
let centerDelta = itemFrame.midX - viewportCenter
let position = centerDelta / (availableSize.width * 0.75)
i += 1
if !isSwipingActive && abs(position) > 0.5 {
continue
} else if isSwipingActive && abs(position) > 1.5 {
continue
}
validIds.append(item.id)
let itemView: ComponentHostView<EnvironmentType>
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<EnvironmentType>()
self.itemViews[item.id] = itemView
self.scrollView.addSubview(itemView)
}
let environment = environment[EnvironmentType.self]
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(GiftViewSheetComponent(
context: component.context,
subject: item.subject
)),
environment: { environment },
containerSize: availableSize
)
itemView.frame = itemFrame
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,278 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import TelegramPresentationData
import MultilineTextComponent
final class TableComponent: CombinedComponent {
class Item: Equatable {
public let id: AnyHashable
public let title: String?
public let hasBackground: Bool
public let component: AnyComponent<Empty>
public let insets: UIEdgeInsets?
public init<IdType: Hashable>(id: IdType, title: String?, hasBackground: Bool = false, component: AnyComponent<Empty>, insets: UIEdgeInsets? = nil) {
self.id = AnyHashable(id)
self.title = title
self.hasBackground = hasBackground
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.hasBackground != rhs.hasBackground {
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 lastBackground = 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)
let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
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
var hasLastBackground = false
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)
}
if item.hasBackground {
hasLastBackground = true
}
i += 1
}
if hasLastBackground {
let lastRowHeight = rowHeights[i - 1] ?? 0
let lastBackground = lastBackground.update(
component: Rectangle(color: secondaryBackgroundColor),
availableSize: CGSize(width: context.availableSize.width, height: lastRowHeight),
transition: context.transition
)
context.add(
lastBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight - lastRowHeight / 2.0))
)
}
let leftColumnBackground = leftColumnBackground.update(
component: Rectangle(color: secondaryBackgroundColor),
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 = 10.0
borderImage = generateImage(CGSize(width: 24.0, height: 24.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: 10, topCapHeight: 10)
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 {
if hasLastBackground {
valueFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - valueChild.size.width) / 2.0), 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)
}
}
}

View File

@ -230,7 +230,7 @@ public enum MediaCropOrientation: Int32 {
}
}
public final class MediaEditorValues: Codable, Equatable {
public final class MediaEditorValues: Codable, Equatable, CustomStringConvertible {
public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool {
if lhs.peerId != rhs.peerId {
return false
@ -1010,6 +1010,114 @@ public final class MediaEditorValues: Codable, Equatable {
}
return false
}
public var description: String {
var components: [String] = []
components.append("originalDimensions: \(self.originalDimensions.width)x\(self.originalDimensions.height)")
if self.cropOffset != .zero {
components.append("cropOffset: \(cropOffset)")
}
if let cropRect = self.cropRect {
components.append("cropRect: \(cropRect)")
}
if self.cropScale != 1.0 {
components.append("cropScale: \(self.cropScale)")
}
if self.cropRotation != 0.0 {
components.append("cropRotation: \(self.cropRotation)")
}
if self.cropMirroring {
components.append("cropMirroring: true")
}
if let cropOrientation = self.cropOrientation {
components.append("cropOrientation: \(cropOrientation)")
}
if let gradientColors = self.gradientColors, !gradientColors.isEmpty {
components.append("gradientColors: \(gradientColors.count) colors")
}
if let videoTrimRange = self.videoTrimRange {
components.append("videoTrimRange: \(videoTrimRange.lowerBound) - \(videoTrimRange.upperBound)")
}
if self.videoIsMuted {
components.append("videoIsMuted: true")
}
if self.videoIsFullHd {
components.append("videoIsFullHd: true")
}
if self.videoIsMirrored {
components.append("videoIsMirrored: true")
}
if let videoVolume = self.videoVolume, videoVolume != 1.0 {
components.append("videoVolume: \(videoVolume)")
}
if let additionalVideoPath = self.additionalVideoPath {
components.append("additionalVideo: \(additionalVideoPath)")
}
if let position = self.additionalVideoPosition {
components.append("additionalVideoPosition: \(position)")
}
if let scale = self.additionalVideoScale {
components.append("additionalVideoScale: \(scale)")
}
if let rotation = self.additionalVideoRotation {
components.append("additionalVideoRotation: \(rotation)")
}
if !self.additionalVideoPositionChanges.isEmpty {
components.append("additionalVideoPositionChanges: \(additionalVideoPositionChanges.count) changes")
}
if !self.collage.isEmpty {
components.append("collage: \(collage.count) items")
}
if self.nightTheme {
components.append("nightTheme: true")
}
if self.drawing != nil {
components.append("drawing: true")
}
if self.maskDrawing != nil {
components.append("maskDrawing: true")
}
if !self.entities.isEmpty {
components.append("entities: \(self.entities.count) items")
}
if !self.toolValues.isEmpty {
components.append("toolValues: \(self.toolValues.count) tools")
}
if let audioTrack = self.audioTrack {
components.append("audioTrack: \(audioTrack.path)")
}
if let qualityPreset = self.qualityPreset {
components.append("qualityPreset: \(qualityPreset)")
}
return "MediaEditorValues(\(components.joined(separator: ", ")))"
}
}
public struct TintValue: Equatable, Codable {

View File

@ -264,6 +264,11 @@ public final class MediaEditorVideoExport {
self.outputPath = outputPath
self.textScale = textScale
Logger.shared.log("VideoExport", "Init")
Logger.shared.log("VideoExport", "Subject: \(subject)")
Logger.shared.log("VideoExport", "Output Path: \(outputPath)")
Logger.shared.log("VideoExport", "Configuration: \(configuration)")
if FileManager.default.fileExists(atPath: outputPath) {
try? FileManager.default.removeItem(atPath: outputPath)
}
@ -297,6 +302,9 @@ public final class MediaEditorVideoExport {
}
private func setup() {
Logger.shared.log("VideoExport", "Setting up")
var mainAsset: AVAsset?
var signals: [Signal<Input, NoError>] = []
@ -948,11 +956,6 @@ public final class MediaEditorVideoExport {
return false
}
}
} else {
// if !writer.appendVideoBuffer(sampleBuffer) {
// writer.markVideoAsFinished()
// return false
// }
}
}
return true
@ -983,17 +986,21 @@ public final class MediaEditorVideoExport {
}
private func start() {
Logger.shared.log("VideoExport", "Start")
guard self.internalStatus == .idle, let writer = self.writer else {
Logger.shared.log("VideoExport", "Failed with invalid state")
self.statusValue = .failed(.invalid)
return
}
guard writer.startWriting() else {
Logger.shared.log("VideoExport", "Failed on startWriting")
self.statusValue = .failed(.writing(nil))
return
}
if let reader = self.reader, !reader.startReading() {
Logger.shared.log("VideoExport", "Failed on startReading")
self.statusValue = .failed(.reading(nil))
return
}
@ -1067,6 +1074,7 @@ public final class MediaEditorVideoExport {
}
if cancelled {
Logger.shared.log("VideoExport", "Cancelled")
try? FileManager.default.removeItem(at: outputUrl)
self.internalStatus = .finished
self.statusValue = .failed(.cancelled)
@ -1108,6 +1116,7 @@ public final class MediaEditorVideoExport {
let exportDuration = end - self.startTimestamp
print("video processing took \(exportDuration)s")
if duration.seconds > 0 {
Logger.shared.log("VideoExport", "Completed with path \(self.outputPath)")
Logger.shared.log("VideoExport", "Video processing took \(exportDuration / duration.seconds)")
}
})

View File

@ -67,6 +67,7 @@ swift_library(
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
"//submodules/TelegramNotices",
],
visibility = [
"//visibility:public",

View File

@ -49,6 +49,7 @@ import StickerPickerScreen
import UIKitRuntimeUtils
import ImageObjectSeparation
import SaveProgressScreen
import TelegramNotices
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@ -58,6 +59,7 @@ private let drawButtonTag = GenericComponentViewTag()
private let textButtonTag = GenericComponentViewTag()
private let stickerButtonTag = GenericComponentViewTag()
private let dayNightButtonTag = GenericComponentViewTag()
private let selectionButtonTag = GenericComponentViewTag()
final class MediaEditorScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -2320,7 +2322,8 @@ final class MediaEditorScreenComponent: Component {
controller.hapticFeedback.impact(.light)
}
},
animateAlpha: false
animateAlpha: false,
tag: selectionButtonTag
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
@ -4744,6 +4747,33 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.controller?.present(tooltipController, in: .current)
}
private var displayedSelectionTooltip = false
func presentSelectionTooltip() {
guard let sourceView = self.componentHost.findTaggedView(tag: selectionButtonTag), !self.displayedSelectionTooltip, self.items.count > 1 else {
return
}
self.displayedSelectionTooltip = true
let _ = (ApplicationSpecificNotice.getMultipleStoriesTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
guard let self, count < 3 else {
return
}
let parentFrame = self.view.convert(self.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize())
let text = self.presentationData.strings.Story_Editor_TooltipSelection(Int32(self.items.count))
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), location: .point(location, .bottom), displayDuration: .default, inset: 8.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
self.controller?.present(tooltipController, in: .current)
let _ = ApplicationSpecificNotice.incrementMultipleStoriesTooltip(accountManager: self.context.sharedContext.accountManager).start()
})
}
fileprivate weak var saveTooltip: SaveProgressScreen?
func presentSaveTooltip() {
guard let controller = self.controller else {
@ -5725,6 +5755,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if hasAppeared && !self.hasAppeared {
self.hasAppeared = hasAppeared
self.presentSelectionTooltip()
}
let componentSize = self.componentHost.update(

View File

@ -597,7 +597,7 @@ extension MediaEditorScreenImpl {
orderedResults.append(item)
}
}
self.completion(results, { [weak self] finished in
self.completion(orderedResults, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
@ -737,7 +737,10 @@ extension MediaEditorScreenImpl {
DispatchQueue.main.async {
if let image {
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
if itemMediaEditor.values.gradientColors == nil {
itemMediaEditor.setGradientColors(mediaEditorGetGradientColors(from: image))
}
if let resultImage = itemMediaEditor.resultImage {
makeEditorImageComposition(
context: self.node.ciContext,

View File

@ -825,6 +825,14 @@ public final class MediaScrubberComponent: Component {
transition: transition
)
}
} else {
for (_ , trackView) in self.trackViews {
trackView.updateTrimEdges(
left: leftHandleFrame.minX,
right: rightHandleFrame.maxX,
transition: transition
)
}
}
let isDraggingTracks = self.trackViews.values.contains(where: { $0.isDragging })
@ -863,7 +871,6 @@ public final class MediaScrubberComponent: Component {
transition.setFrame(view: self.cursorImageView, frame: CGRect(origin: .zero, size: self.cursorView.frame.size))
if let (coverPosition, coverImage) = component.cover {
let imageSize = CGSize(width: 36.0, height: 36.0)
var animateFrame = false
@ -964,6 +971,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
fileprivate let audioIconView: UIImageView
fileprivate let audioTitle = ComponentView<Empty>()
fileprivate let segmentsContainerView = UIView()
fileprivate var segmentTitles: [Int32: ComponentView<Empty>] = [:]
fileprivate var segmentLayers: [Int32: SimpleLayer] = [:]
@ -1037,7 +1045,10 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
self.clippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.containerView)
self.backgroundView.addSubview(self.vibrancyView)
self.segmentsContainerView.clipsToBounds = true
self.segmentsContainerView.isUserInteractionEnabled = false
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGesture)
@ -1133,6 +1144,25 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
}
}
private var leftTrimEdge: CGFloat?
private var rightTrimEdge: CGFloat?
func updateTrimEdges(
left: CGFloat,
right: CGFloat,
transition: ComponentTransition
) {
self.leftTrimEdge = left
self.rightTrimEdge = right
if let params = self.params {
self.updateSegmentContainer(
scrubberSize: CGSize(width: params.availableSize.width, height: trackHeight),
availableSize: params.availableSize,
transition: transition
)
}
}
private func updateThumbnailContainers(
scrubberSize: CGSize,
availableSize: CGSize,
@ -1146,6 +1176,17 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
transition.setBounds(view: self.videoOpaqueFramesContainer, bounds: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge, height: scrubberSize.height)))
}
private func updateSegmentContainer(
scrubberSize: CGSize,
availableSize: CGSize,
transition: ComponentTransition
) {
let containerLeftEdge: CGFloat = self.leftTrimEdge ?? 0.0
let containerRightEdge: CGFloat = self.rightTrimEdge ?? availableSize.width
transition.setFrame(view: self.segmentsContainerView, frame: CGRect(origin: CGPoint(x: containerLeftEdge, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge - 2.0, height: scrubberSize.height)))
}
func update(
context: AccountContext,
style: MediaScrubberComponent.Style,
@ -1281,6 +1322,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
if self.videoTransparentFramesContainer.superview == nil {
self.containerView.addSubview(self.videoTransparentFramesContainer)
self.containerView.addSubview(self.videoOpaqueFramesContainer)
self.containerView.addSubview(self.segmentsContainerView)
}
var previousFramesUpdateTimestamp: Double?
if let previousParams, case let .video(_, previousFramesUpdateTimestampValue) = previousParams.track.content {
@ -1333,6 +1375,12 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
transition: transition
)
self.updateSegmentContainer(
scrubberSize: scrubberSize,
availableSize: availableSize,
transition: transition
)
var frameAspectRatio = 0.66
if let image = frames.first, image.size.height > 0.0 {
frameAspectRatio = max(0.66, image.size.width / image.size.height)
@ -1488,9 +1536,8 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
self.backgroundView.update(size: containerFrame.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.vibrancyView, frame: CGRect(origin: .zero, size: containerFrame.size))
transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: .zero, size: containerFrame.size))
var segmentCount = 0
var segmentOrigin: CGFloat = 0.0
var segmentWidth: CGFloat = 0.0
if let segmentDuration {
if duration > segmentDuration {
@ -1499,17 +1546,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
segmentWidth = floorToScreenPixels(containerFrame.width * fraction)
}
if let trimRange = track.trimRange {
if trimRange.lowerBound > 0.0 {
let fraction = trimRange.lowerBound / duration
segmentOrigin = floorToScreenPixels(containerFrame.width * fraction)
}
let actualSegmentCount = Int(ceil((trimRange.upperBound - trimRange.lowerBound) / segmentDuration)) - 1
segmentCount = min(actualSegmentCount, segmentCount)
}
}
let displaySegmentLabels = segmentWidth >= 30.0
var validIds = Set<Int32>()
var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height)
var segmentFrame = CGRect(x: segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height)
for i in 0 ..< min(segmentCount, 2) {
let id = Int32(i)
validIds.insert(id)
@ -1530,7 +1575,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
self.segmentLayers[id] = segmentLayer
self.segmentTitles[id] = segmentTitle
self.containerView.layer.addSublayer(segmentLayer)
self.segmentsContainerView.layer.addSublayer(segmentLayer)
}
transition.setFrame(layer: segmentLayer, frame: segmentFrame)
@ -1546,8 +1591,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
containerSize: containerFrame.size
)
if let view = segmentTitle.view {
view.alpha = displaySegmentLabels ? 1.0 : 0.0
if view.superview == nil {
self.containerView.addSubview(view)
self.segmentsContainerView.addSubview(view)
}
segmentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: segmentFrame.maxX + 2.0, y: 2.0), size: segmentTitleSize))
}

View File

@ -184,7 +184,9 @@ public final class PeerInfoGiftsCoverComponent: Component {
}
}
private var scheduledAnimateIn = false
public func willAnimateIn() {
self.scheduledAnimateIn = true
for (_, layer) in self.iconLayers {
layer.opacity = 0.0
}
@ -194,6 +196,7 @@ public final class PeerInfoGiftsCoverComponent: Component {
guard let _ = self.currentSize, let component = self.component else {
return
}
self.scheduledAnimateIn = false
for (_, layer) in self.iconLayers {
layer.opacity = 1.0
@ -319,8 +322,12 @@ public final class PeerInfoGiftsCoverComponent: Component {
self.iconLayers[id] = iconLayer
self.layer.addSublayer(iconLayer)
iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
if self.scheduledAnimateIn {
iconLayer.opacity = 0.0
} else {
iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
iconLayer.startAnimations(index: index)
}
@ -349,7 +356,10 @@ public final class PeerInfoGiftsCoverComponent: Component {
iconTransition.setPosition(layer: iconLayer, position: absolutePosition)
iconLayer.updateRotation(effectiveAngle, transition: iconTransition)
iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction))
iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction)
if !self.scheduledAnimateIn {
iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction)
}
index += 1
}

View File

@ -4978,7 +4978,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.refreshMessageTagStatsDisposable = context.engine.messages.refreshMessageTagStats(peerId: peerId, threadId: chatLocation.threadId, tags: [.video, .photo, .gif, .music, .voiceOrInstantVideo, .webPage, .file]).startStrict()
if peerId.namespace == Namespaces.Peer.CloudChannel {
self.translationStateDisposable = (chatTranslationState(context: context, peerId: peerId)
self.translationStateDisposable = (chatTranslationState(context: context, peerId: peerId, threadId: nil)
|> deliverOnMainQueue).startStrict(next: { [weak self] translationState in
self?.translationState = translationState
})
@ -6507,7 +6507,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
f(.dismissWithoutContent)
if let strongSelf = self {
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, threadId: nil, { current in
return current?.withIsEnabled(true)
}).startStandalone()
@ -6698,7 +6698,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
f(.dismissWithoutContent)
if let strongSelf = self {
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, threadId: nil, { current in
return current?.withIsEnabled(true)
}).startStandalone()
@ -6957,7 +6957,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
f(.dismissWithoutContent)
if let strongSelf = self {
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, threadId: nil, { current in
return current?.withIsEnabled(true)
}).startStandalone()

View File

@ -347,7 +347,7 @@ extension PeerInfoScreenImpl {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom)
if [.suggest, .fallback].contains(mode) {
} else {

View File

@ -488,7 +488,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
switch product.gift {
case let .generic(gift):
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
subject = .starGift(gift: gift, price: "# \(gift.price)")
peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous
if let availability = gift.availability {
@ -571,10 +571,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
} else {
let allSubjects: [GiftViewScreen.Subject] = (self.starsProducts ?? []).map { .profileGift(self.peerId, $0) }
let index = self.starsProducts?.firstIndex(where: { $0 == product }) ?? 0
var dismissImpl: (() -> Void)?
let controller = GiftViewScreen(
context: self.context,
subject: .profileGift(self.peerId, product),
allSubjects: allSubjects,
index: index,
updateSavedToProfile: { [weak self] reference, added in
guard let self else {
return

View File

@ -22,17 +22,20 @@ private final class SheetContent: CombinedComponent {
let context: AccountContext
let configuration: AccountFreezeConfiguration
let openTerms: () -> Void
let submitAppeal: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
configuration: AccountFreezeConfiguration,
openTerms: @escaping () -> Void,
submitAppeal: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.configuration = configuration
self.openTerms = openTerms
self.submitAppeal = submitAppeal
self.dismiss = dismiss
}
@ -132,10 +135,14 @@ private final class SheetContent: CombinedComponent {
component: AnyComponent(ParagraphComponent(
title: strings.FrozenAccount_Violation_Title,
titleColor: textColor,
text: strings.FrozenAccount_Violation_Text,
text: strings.FrozenAccount_Violation_TextNew,
textColor: secondaryTextColor,
iconName: "Account Freeze/Violation",
iconColor: linkColor
iconColor: linkColor,
action: {
component.openTerms()
component.dismiss()
}
))
)
)
@ -257,15 +264,18 @@ private final class SheetContainerComponent: CombinedComponent {
let context: AccountContext
let configuration: AccountFreezeConfiguration
let openTerms: () -> Void
let submitAppeal: () -> Void
init(
context: AccountContext,
configuration: AccountFreezeConfiguration,
openTerms: @escaping () -> Void,
submitAppeal: @escaping () -> Void
) {
self.context = context
self.configuration = configuration
self.openTerms = openTerms
self.submitAppeal = submitAppeal
}
@ -292,6 +302,7 @@ private final class SheetContainerComponent: CombinedComponent {
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
configuration: context.component.configuration,
openTerms: context.component.openTerms,
submitAppeal: context.component.submitAppeal,
dismiss: {
animateOut.invoke(Action { _ in
@ -367,12 +378,16 @@ public final class AccountFreezeInfoScreen: ViewControllerComponentContainer {
self.context = context
let configuration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var openTermsImpl: (() -> Void)?
var submitAppealImpl: (() -> Void)?
super.init(
context: context,
component: SheetContainerComponent(
context: context,
configuration: configuration,
openTerms: {
openTermsImpl?()
},
submitAppeal: {
submitAppealImpl?()
}
@ -384,6 +399,15 @@ public final class AccountFreezeInfoScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
openTermsImpl = { [weak self] in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
Queue.mainQueue().after(0.4) {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.FrozenAccount_Violation_TextNew_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
submitAppealImpl = { [weak self] in
guard let self, let navigationController = self.navigationController as? NavigationController, let url = configuration.freezeAppealUrl else {
return

View File

@ -249,6 +249,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
} else {
textString = strings.Stars_Purchase_SendGroupMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
}
case .buyStarGift:
textString = strings.Stars_Purchase_BuyStarGiftInfo
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
@ -306,6 +308,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
var i = 0
var items: [AnyComponentWithIdentity<Empty>] = []
var collapsedItems = 0
if let products = state.products, let balance = context.component.balance {
var minimumCount: StarsAmount?
if let requiredStars = context.component.purpose.requiredStars {
@ -324,6 +327,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
if let _ = minimumCount, items.isEmpty {
} else if !context.component.expanded && product.isExtended {
collapsedItems += 1
continue
}
@ -388,7 +392,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
}
}
if !context.component.expanded && items.count > 1 {
if !context.component.expanded && collapsedItems > 0 {
let titleComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Stars_Purchase_ShowMore,
@ -830,7 +834,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
titleText = strings.Stars_Purchase_GetStars
case .gift:
titleText = strings.Stars_Purchase_GiftStars
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars):
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
@ -1280,6 +1284,8 @@ private extension StarsPurchasePurpose {
return requiredStars
case let .sendMessage(_, requiredStars):
return requiredStars
case let .buyStarGift(requiredStars):
return requiredStars
default:
return nil
}

View File

@ -447,7 +447,11 @@ final class StarsTransactionsScreenComponent: Component {
let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16.0 * 2.0
let bottomInset: CGFloat = environment.safeInsets.bottom
contentHeight += environment.statusBarHeight
if environment.statusBarHeight > 0.0 {
contentHeight += environment.statusBarHeight
} else {
contentHeight += 12.0
}
let starTransition: ComponentTransition = .immediate

View File

@ -55,6 +55,7 @@ private final class SheetContent: CombinedComponent {
let closeButton = Child(Button.self)
let title = Child(Text.self)
let amountSection = Child(ListSectionComponent.self)
let amountAdditionalLabel = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self)
@ -100,7 +101,8 @@ private final class SheetContent: CombinedComponent {
let titleString: String
let amountTitle: String
let amountPlaceholder: String
let amountLabel: String?
var amountLabel: String?
var amountRightLabel: String?
let minAmount: StarsAmount?
let maxAmount: StarsAmount?
@ -116,7 +118,6 @@ private final class SheetContent: CombinedComponent {
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = status.balances.availableBalance
amountLabel = nil
case .accountWithdraw:
titleString = environment.strings.Stars_Withdraw_Title
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
@ -124,7 +125,6 @@ private final class SheetContent: CombinedComponent {
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = state.balance
amountLabel = nil
case .paidMedia:
titleString = environment.strings.Stars_PaidContent_Title
amountTitle = environment.strings.Stars_PaidContent_AmountTitle
@ -136,8 +136,6 @@ private final class SheetContent: CombinedComponent {
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
amountLabel = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
} else {
amountLabel = nil
}
case .reaction:
titleString = environment.strings.Stars_SendStars_Title
@ -146,7 +144,6 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
amountLabel = nil
case let .starGiftResell(update):
titleString = update ? environment.strings.Stars_SellGift_EditTitle : environment.strings.Stars_SellGift_Title
amountTitle = environment.strings.Stars_SellGift_AmountTitle
@ -154,7 +151,6 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
amountLabel = nil
case let .paidMessages(_, minAmountValue, _, kind):
//TODO:localize
switch kind {
@ -168,7 +164,6 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: minAmountValue, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.paidMessageMaxAmount, nanos: 0)
amountLabel = nil
}
let title = title.update(
@ -287,6 +282,11 @@ private final class SheetContent: CombinedComponent {
let starsValue = Int32(floor(Float(value) * Float(resaleConfiguration.paidMessageCommissionPermille) / 1000.0))
let starsString = environment.strings.Stars_SellGift_AmountInfo_Stars(starsValue)
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(starsString).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
amountRightLabel = "\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
}
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.paidMessageCommissionPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural))
}
@ -355,8 +355,17 @@ private final class SheetContent: CombinedComponent {
.cornerRadius(10.0)
)
contentSize.height += amountSection.size.height
if let amountRightLabel {
let amountAdditionalLabel = amountAdditionalLabel.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: amountRightLabel, font: amountFont, textColor: amountTextColor))),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(amountAdditionalLabel
.position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0)))
}
contentSize.height += 32.0
let buttonString: String
if case .paidMedia = component.mode {
buttonString = environment.strings.Stars_PaidContent_Create

View File

@ -117,6 +117,8 @@ public final class TabSelectorComponent: Component {
private let selectionView: UIImageView
private var visibleItems: [AnyHashable: VisibleItem] = [:]
private var didInitiallyScroll = false
override init(frame: CGRect) {
self.selectionView = UIImageView()
@ -238,11 +240,15 @@ public final class TabSelectorComponent: Component {
)),
effectAlignment: .center,
minSize: nil,
action: { [weak self] in
action: { [weak self, weak itemView] in
guard let self, let component = self.component else {
return
}
component.setSelectedId(itemId)
if let view = itemView?.title.view, allowScroll && self.contentSize.width > self.bounds.width {
self.scrollRectToVisible(view.frame.insetBy(dx: -64.0, dy: 0.0), animated: true)
}
},
animateScale: !isLineSelection
)),
@ -336,11 +342,15 @@ public final class TabSelectorComponent: Component {
self.selectionView.alpha = 0.0
}
self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
let contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
if self.contentSize != contentSize {
self.contentSize = contentSize
}
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
if let selectedBackgroundRect, self.bounds.width > 0.0 {
if let selectedBackgroundRect, self.bounds.width > 0.0 && !self.didInitiallyScroll {
self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false)
self.didInitiallyScroll = true
}
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0)

View File

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "ic_qrcode.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -681,6 +681,7 @@ extension ChatControllerImpl {
let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId))
|> distinctUntilChanged
let chatLocation = self.chatLocation
self.translationStateDisposable = (combineLatest(
queue: .concurrentDefaultQueue(),
isPremium,
@ -693,7 +694,7 @@ extension ChatControllerImpl {
maybeSuggestPremium = true
}
if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden {
return chatTranslationState(context: context, peerId: peerId)
return chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId)
|> map { translationState -> ChatPresentationTranslationState? in
if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) {
return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode)
@ -4130,7 +4131,8 @@ extension ChatControllerImpl {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
}, openSuggestPost: { [weak self] in
guard let self else {
let _ = self
/*guard let self else {
return
}
guard let peerId = self.chatLocation.peerId else {
@ -4151,7 +4153,7 @@ extension ChatControllerImpl {
)
chatController.navigationPresentation = .modal
self.push(chatController)
self.push(chatController)*/
}, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self {
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
@ -4357,32 +4359,32 @@ extension ChatControllerImpl {
}
let _ = strongSelf.context.engine.peers.setForumChannelTopicClosed(id: peerId, threadId: threadId, isClosed: false).startStandalone()
}, toggleTranslation: { [weak self] type in
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
guard let self, let peerId = self.chatLocation.peerId else {
return
}
let _ = (updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in
let _ = (updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
return current?.withIsEnabled(type == .translated)
})
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
if let strongSelf = self, type == .translated {
if let self, type == .translated {
Queue.mainQueue().after(0.15) {
strongSelf.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
}
}
})
}, changeTranslationLanguage: { [weak self] langCode in
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
guard let self, let peerId = self.chatLocation.peerId else {
return
}
let langCode = normalizeTranslationLanguage(langCode)
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in
let _ = updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
return current?.withToLang(langCode).withIsEnabled(true)
}).startStandalone()
}, addDoNotTranslateLanguage: { [weak self] langCode in
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
guard let self, let peerId = self.chatLocation.peerId else {
return
}
let _ = updateTranslationSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
let _ = updateTranslationSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in
var updated = current
if var ignoredLanguages = updated.ignoredLanguages {
if !ignoredLanguages.contains(langCode) {
@ -4391,7 +4393,7 @@ extension ChatControllerImpl {
updated.ignoredLanguages = ignoredLanguages
} else {
var ignoredLanguages = Set<String>()
ignoredLanguages.insert(strongSelf.presentationData.strings.baseLanguageCode)
ignoredLanguages.insert(self.presentationData.strings.baseLanguageCode)
for language in systemLanguageCodes() {
ignoredLanguages.insert(language)
}
@ -4400,11 +4402,11 @@ extension ChatControllerImpl {
}
return updated
}).startStandalone()
let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in
let _ = updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
return nil
}).startStandalone()
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var languageCode = presentationData.strings.baseLanguageCode
let rawSuffix = "-raw"
if languageCode.hasSuffix(rawSuffix) {
@ -4413,11 +4415,11 @@ extension ChatControllerImpl {
let locale = Locale(identifier: languageCode)
let fromLanguage: String = locale.localizedString(forLanguageCode: langCode) ?? ""
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: .white)!, title: nil, text: presentationData.strings.Conversation_Translation_AddedToDoNotTranslateText(fromLanguage).string, round: false, undoText: presentationData.strings.Conversation_Translation_Settings), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action, let strongSelf = self {
let controller = translationSettingsController(context: strongSelf.context)
self.present(UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: .white)!, title: nil, text: presentationData.strings.Conversation_Translation_AddedToDoNotTranslateText(fromLanguage).string, round: false, undoText: presentationData.strings.Conversation_Translation_Settings), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action, let self {
let controller = translationSettingsController(context: self.context)
controller.navigationPresentation = .modal
strongSelf.push(controller)
self.push(controller)
}
return true
}), in: .current)

View File

@ -1655,7 +1655,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let translationState: Signal<ChatTranslationState?, NoError>
if let peerId = chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat && peerId != context.account.peerId && subject != .scheduledMessages {
translationState = chatTranslationState(context: context, peerId: peerId)
translationState = chatTranslationState(context: context, peerId: peerId, threadId: self.chatLocation.threadId)
} else {
translationState = .single(nil)
}

View File

@ -115,7 +115,7 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode {
}
}
let size = CGSize(width: params.width - params.additionalSideInsets.left * 2.0 - params.leftInset * 2.0, height: height)
let size = CGSize(width: params.width - params.additionalSideInsets.left * 2.0 - params.leftInset * 2.0 - 32.0, height: height)
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
@ -136,7 +136,7 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode {
if buttonView.superview == nil {
self.view.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(), size: buttonSize))
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - buttonSize.width) / 2.0), y: 0.0), size: buttonSize))
}
return height

View File

@ -1,4 +1,5 @@
import Foundation
import UniformTypeIdentifiers
import UIKit
import Display
import AsyncDisplayKit
@ -44,6 +45,7 @@ import TelegramNotices
import AnimatedCountLabelNode
import TelegramStringFormatting
import TextNodeWithEntities
import DeviceModel
private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
@ -4473,10 +4475,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
var attributedString: NSAttributedString?
if let data = pasteboard.data(forPasteboardType: "private.telegramtext"), let value = chatInputStateStringFromAppSpecificString(data: data) {
attributedString = value
} else if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
} else if let data = pasteboard.data(forPasteboardType: "public.rtf") {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
if let _ = pasteboard.data(forPasteboardType: "com.apple.notes.richtext"), DeviceModel.current.isIpad, let htmlData = pasteboard.data(forPasteboardType: "public.html") {
attributedString = chatInputStateStringFromRTF(htmlData, type: NSAttributedString.DocumentType.html)
} else {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
}
}
if let attributedString = attributedString {

View File

@ -802,7 +802,6 @@ func openResolvedUrlImpl(
}
if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let controller = UndoOverlayController(
presentationData: presentationData,
content: .universal(
@ -810,8 +809,8 @@ func openResolvedUrlImpl(
scale: 0.066,
colors: [:],
title: nil,
text: "You have enough stars at the moment.",
customUndoText: "Buy Anyway",
text: presentationData.strings.Stars_Purchase_EnoughStars,
customUndoText: presentationData.strings.Stars_Purchase_BuyAnyway,
timeout: nil
),
elevatedLayout: true,
@ -826,6 +825,15 @@ func openResolvedUrlImpl(
proceed()
}
}
case .stars:
dismissInput()
if let starsContext = context.starsContext {
let controller = context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext)
controller.navigationPresentation = .modal
if let navigationController {
navigationController.pushViewController(controller, animated: true)
}
}
case let .joinVoiceChat(peerId, invite):
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in

View File

@ -1016,7 +1016,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
}
} else {
if parsedUrl.host == "importStickers" {
if parsedUrl.host == "stars" {
handleResolvedUrl(.stars)
} else if parsedUrl.host == "importStickers" {
handleResolvedUrl(.importStickers)
} else if parsedUrl.host == "settings" {
if let path = parsedUrl.pathComponents.last {

View File

@ -1631,7 +1631,6 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
tgcalls::Register<tgcalls::InstanceImpl>();
//tgcalls::Register<tgcalls::InstanceV2_4_0_0Impl>();
tgcalls::Register<tgcalls::InstanceV2Impl>();
tgcalls::Register<tgcalls::InstanceV2ReferenceImpl>();
});

View File

@ -75,9 +75,12 @@ public struct ChatTranslationState: Codable {
}
}
private func cachedChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id) -> Signal<ChatTranslationState?, NoError> {
private func cachedChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?) -> Signal<ChatTranslationState?, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
if let threadId {
key.setInt64(8, value: threadId)
}
return engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.translationState, id: key))
|> map { entry -> ChatTranslationState? in
@ -85,9 +88,12 @@ private func cachedChatTranslationState(engine: TelegramEngine, peerId: EnginePe
}
}
private func updateChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id, state: ChatTranslationState?) -> Signal<Never, NoError> {
private func updateChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?, state: ChatTranslationState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
if let threadId {
key.setInt64(8, value: threadId)
}
if let state {
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.translationState, id: key, item: state)
@ -96,9 +102,12 @@ private func updateChatTranslationState(engine: TelegramEngine, peerId: EnginePe
}
}
public func updateChatTranslationStateInteractively(engine: TelegramEngine, peerId: EnginePeer.Id, _ f: @escaping (ChatTranslationState?) -> ChatTranslationState?) -> Signal<Never, NoError> {
public func updateChatTranslationStateInteractively(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?, _ f: @escaping (ChatTranslationState?) -> ChatTranslationState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
if let threadId {
key.setInt64(8, value: threadId)
}
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.translationState, id: key))
|> map { entry -> ChatTranslationState? in
@ -106,7 +115,7 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer
}
|> mapToSignal { current -> Signal<Never, NoError> in
if let current {
return updateChatTranslationState(engine: engine, peerId: peerId, state: f(current))
return updateChatTranslationState(engine: engine, peerId: peerId, threadId: threadId, state: f(current))
} else {
return .never()
}
@ -166,7 +175,7 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess
} |> switchToLatest
}
public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) -> Signal<ChatTranslationState?, NoError> {
public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64?) -> Signal<ChatTranslationState?, NoError> {
if peerId.id == EnginePeer.Id.Id._internalFromInt64Value(777000) {
return .single(nil)
}
@ -202,7 +211,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
}
}
return cachedChatTranslationState(engine: context.engine, peerId: peerId)
return cachedChatTranslationState(engine: context.engine, peerId: peerId, threadId: threadId)
|> mapToSignal { cached in
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 {
@ -214,7 +223,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
} else {
return .single(nil)
|> then(
context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 32, fixedCombinedReadStates: nil)
context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: threadId), index: .upperBound, anchorIndex: .upperBound, count: 32, fixedCombinedReadStates: nil)
|> filter { messageHistoryView -> Bool in
return messageHistoryView.0.entries.count > 1
}
@ -308,7 +317,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
toLang: cached?.toLang,
isEnabled: isEnabled
)
let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start()
let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, threadId: threadId, state: state).start()
if !dontTranslateLanguages.contains(fromLang) {
return state
} else {

View File

@ -828,7 +828,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
if let webView = self.webView {
var scrollInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0)
let inputHeight = self.validLayout?.0.inputHeight ?? 0.0
let intrinsicBottomInset = layout.intrinsicInsets.bottom > 40.0 ? layout.intrinsicInsets.bottom : 0.0
var scrollInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(inputHeight, intrinsicBottomInset), right: 0.0)
var frameBottomInset: CGFloat = 0.0
if scrollInset.bottom > 40.0 {
frameBottomInset = scrollInset.bottom
@ -841,12 +845,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
if !webView.frame.width.isZero && webView.frame != webViewFrame {
self.updateWebViewWhenStable = true
}
var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
bottomInset = max(bottomInset, inputHeight)
var viewportBottomInset = max(frameBottomInset, scrollInset.bottom)
if (self.validLayout?.0.inputHeight ?? 0.0) < 44.0 {
viewportBottomInset += layout.additionalInsets.bottom
}
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset)))
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - viewportBottomInset)))
if webView.scrollView.contentInset != scrollInset {
webView.scrollView.contentInset = scrollInset
@ -895,8 +899,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let controller = self.controller {
webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition)
let contentInsetsData = "{top:\(contentTopInset), bottom:0.0, left:0.0, right:0.0}"
webView.sendEvent(name: "content_safe_area_changed", data: contentInsetsData)
let data: JSON = [
"top": Double(contentTopInset),
"bottom": 0.0,
"left": 0.0,
"right": 0.0
]
webView.sendEvent(name: "content_safe_area_changed", data: data.string)
if self.updateWebViewWhenStable && !controller.isContainerPanning() {
self.updateWebViewWhenStable = false
@ -1056,6 +1065,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
self.lastExpansionTimestamp = currentTimestamp
controller.requestAttachmentMenuExpansion()
Queue.mainQueue().after(0.4) {
self.webView?.setNeedsLayout()
}
}
case "web_app_close":
controller.dismiss()
@ -1333,7 +1346,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.completion = { [weak self] result in
if let strongSelf = self {
if let result = result {
strongSelf.sendQrCodeScannedEvent(data: result)
strongSelf.sendQrCodeScannedEvent(dataString: result)
} else {
strongSelf.sendQrCodeScannerClosedEvent()
}
@ -1923,8 +1936,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
private func sendInvoiceClosedEvent(slug: String, result: InvoiceCloseResult) {
let paramsString = "{slug: \"\(slug)\", status: \"\(result.string)\"}"
self.webView?.sendEvent(name: "invoice_closed", data: paramsString)
let data: JSON = [
"slug": slug,
"status": result.string
]
self.webView?.sendEvent(name: "invoice_closed", data: data.string)
}
fileprivate func sendBackButtonEvent() {
@ -1936,24 +1952,23 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate func sendAlertButtonEvent(id: String?) {
var paramsString: String?
if let id = id {
paramsString = "{button_id: \"\(id)\"}"
var data: [String: Any] = [:]
if let id {
data["button_id"] = id
}
self.webView?.sendEvent(name: "popup_closed", data: paramsString ?? "{}")
}
fileprivate func sendPhoneRequestedEvent(phone: String?) {
var paramsString: String?
if let phone = phone {
paramsString = "{phone_number: \"\(phone)\"}"
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "popup_closed", data: serializedData)
}
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
}
fileprivate func sendQrCodeScannedEvent(data: String?) {
let paramsString = data.flatMap { "{data: \"\($0)\"}" } ?? "{}"
self.webView?.sendEvent(name: "qr_text_received", data: paramsString)
fileprivate func sendQrCodeScannedEvent(dataString: String?) {
var data: [String: Any] = [:]
if let dataString {
data["data"] = dataString
}
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "qr_text_received", data: serializedData)
}
}
fileprivate func sendQrCodeScannerClosedEvent() {
@ -1961,14 +1976,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate func sendClipboardTextEvent(requestId: String, fillData: Bool) {
var paramsString: String
var data: [String: Any] = [:]
data["req_id"] = requestId
if fillData {
let data = UIPasteboard.general.string ?? ""
paramsString = "{req_id: \"\(requestId)\", data: \"\(data)\"}"
} else {
paramsString = "{req_id: \"\(requestId)\"}"
let pasteboardData = UIPasteboard.general.string ?? ""
data["data"] = pasteboardData
}
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "clipboard_text_received", data: serializedData)
}
self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString)
}
fileprivate func requestWriteAccess() {
@ -1977,13 +1993,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"allowed\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "write_access_requested", data: paramsString)
let data: JSON = [
"status": success ? "allowed" : "cancelled"
]
self.webView?.sendEvent(name: "write_access_requested", data: data.string)
}
let _ = (self.context.engine.messages.canBotSendMessages(botId: controller.botId)
@ -2021,13 +2034,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"sent\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
let data: JSON = [
"status": success ? "sent" : "cancelled"
]
self.webView?.sendEvent(name: "phone_requested", data: data.string)
}
let _ = (self.context.engine.data.get(
@ -2348,28 +2358,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = encryptedData
return state
})
var data: [String: Any] = [:]
data["status"] = "updated"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "updated"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
} else {
var data: [String: Any] = [:]
data["status"] = "failed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "failed"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
}
}
}.start()
@ -2379,17 +2376,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = nil
return state
})
var data: [String: Any] = [:]
data["status"] = "removed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "removed"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
}
}
@ -2410,13 +2400,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard controller.isFullscreen != isFullscreen else {
self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}")
let data: JSON = [
"error": "ALREADY_FULLSCREEN"
]
self.webView?.sendEvent(name: "fullscreen_failed", data: data.string)
return
}
let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}"
self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString)
let data: JSON = [
"is_fullscreen": isFullscreen
]
self.webView?.sendEvent(name: "fullscreen_changed", data: data.string)
controller.isFullscreen = isFullscreen
if isFullscreen {
controller.requestAttachmentMenuExpansion()
@ -2436,7 +2431,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var isAccelerometerActive = false
fileprivate func setIsAccelerometerActive(_ isActive: Bool, refreshRate: Double? = nil) {
guard self.motionManager.isAccelerometerAvailable else {
self.webView?.sendEvent(name: "accelerometer_failed", data: "{error: \"UNSUPPORTED\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "accelerometer_failed", data: data.string)
return
}
guard self.isAccelerometerActive != isActive else {
@ -2451,15 +2449,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
self.motionManager.accelerometerUpdateInterval = 1.0
}
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { [weak self] accelerometerData, error in
guard let self, let accelerometerData else {
return
}
let gravityConstant = 9.81
self.webView?.sendEvent(
name: "accelerometer_changed",
data: "{x: \(data.acceleration.x * gravityConstant), y: \(data.acceleration.y * gravityConstant), z: \(data.acceleration.z * gravityConstant)}"
)
let gravityConstant: Double = 9.81
let data: JSON = [
"x": Double(accelerometerData.acceleration.x * gravityConstant),
"y": Double(accelerometerData.acceleration.y * gravityConstant),
"z": Double(accelerometerData.acceleration.z * gravityConstant)
]
self.webView?.sendEvent(name: "accelerometer_changed", data: data.string)
}
} else {
if self.motionManager.isAccelerometerActive {
@ -2472,7 +2472,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var isDeviceOrientationActive = false
fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil, absolute: Bool = false) {
guard self.motionManager.isDeviceMotionAvailable else {
self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "device_orientation_failed", data: data.string)
return
}
guard self.isDeviceOrientationActive != isActive else {
@ -2505,25 +2508,29 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
effectiveIsAbsolute = false
}
self.motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue.main) { [weak self] motionData, error in
guard let self, let motionData else {
return
}
var alpha: Double
if effectiveIsAbsolute {
alpha = data.heading * .pi / 180.0
alpha = motionData.heading * .pi / 180.0
if alpha > .pi {
alpha -= 2.0 * .pi
} else if alpha < -.pi {
alpha += 2.0 * .pi
}
} else {
alpha = data.attitude.yaw
alpha = motionData.attitude.yaw
}
self.webView?.sendEvent(
name: "device_orientation_changed",
data: "{absolute: \(effectiveIsAbsolute ? "true" : "false"), alpha: \(alpha), beta: \(data.attitude.pitch), gamma: \(data.attitude.roll)}"
)
let data: JSON = [
"absolute": effectiveIsAbsolute,
"alpha": Double(alpha),
"beta": Double(motionData.attitude.pitch),
"gamma": Double(motionData.attitude.roll)
]
self.webView?.sendEvent(name: "device_orientation_changed", data: data.string)
}
} else {
if self.motionManager.isDeviceMotionActive {
@ -2536,7 +2543,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var isGyroscopeActive = false
fileprivate func setIsGyroscopeActive(_ isActive: Bool, refreshRate: Double? = nil) {
guard self.motionManager.isGyroAvailable else {
self.webView?.sendEvent(name: "gyroscope_failed", data: "{error: \"UNSUPPORTED\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "gyroscope_failed", data: data.string)
return
}
guard self.isGyroscopeActive != isActive else {
@ -2551,14 +2561,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
self.motionManager.gyroUpdateInterval = 1.0
}
self.motionManager.startGyroUpdates(to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startGyroUpdates(to: OperationQueue.main) { [weak self] gyroData, error in
guard let self, let gyroData else {
return
}
self.webView?.sendEvent(
name: "gyroscope_changed",
data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}"
)
let data: JSON = [
"x": Double(gyroData.rotationRate.x),
"y": Double(gyroData.rotationRate.y),
"z": Double(gyroData.rotationRate.z)
]
self.webView?.sendEvent(name: "gyroscope_changed", data: data.string)
}
} else {
if self.motionManager.isGyroActive {
@ -2575,7 +2587,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
let _ = (self.context.engine.messages.getPreparedInlineMessage(botId: controller.botId, id: id)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] preparedMessage in
guard let self, let controller, let preparedMessage else {
self?.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"MESSAGE_EXPIRED\"}")
let data: JSON = [
"error": "MESSAGE_EXPIRED"
]
self?.webView?.sendEvent(name: "prepared_message_failed", data: data.string)
return
}
let previewController = WebAppMessagePreviewScreen(context: controller.context, botName: controller.botName, botAddress: controller.botAddress, preparedMessage: preparedMessage, completion: { [weak self] result in
@ -2585,7 +2600,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
if result {
self.webView?.sendEvent(name: "prepared_message_sent", data: nil)
} else {
self.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "prepared_message_failed", data: data.string)
}
})
previewController.navigationPresentation = .flatModal
@ -2599,7 +2617,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
guard !fileName.contains("/") && fileName.lengthOfBytes(using: .utf8) < 256 && url.lengthOfBytes(using: .utf8) < 32768 else {
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
return
}
@ -2635,7 +2656,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard canDownload else {
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
return
}
var fileSizeString = ""
@ -2646,14 +2670,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
let text: String = self.presentationData.strings.WebApp_Download_Text(controller.botName, fileName, fileSizeString).string
let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in
self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.WebApp_Download_Download, action: { [weak self] in
self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia)
})
], parseMarkdown: true)
alertController.dismissed = { [weak self] byOutsideTap in
self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}
controller.present(alertController, in: .window(.root))
})
@ -2664,7 +2694,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
guard let controller = self.controller else {
return
}
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"downloading\"}")
let data: JSON = [
"status": "downloading"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
var removeImpl: (() -> Void)?
let fileDownload = FileDownload(
@ -2840,13 +2873,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
demoController?.replace(with: c)
}
controller.parentController()?.push(demoController)
self.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
return
}
let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: botId, enabled: true)
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"allowed\"}")
let data: JSON = [
"status": "allowed"
]
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
})
if let botPeer {
@ -2865,7 +2905,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.present(resultController, in: .window(.root))
}
} else {
self.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
@ -2874,7 +2917,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
)
alertController.dismissed = { [weak self] byOutsideTap in
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
controller.present(alertController, in: .window(.root))
})
@ -2894,7 +2940,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard let file = files[fileId] else {
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
let data: JSON = [
"error": "SUGGESTED_EMOJI_INVALID"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
return
}
let confirmController = WebAppSetEmojiStatusScreen(
@ -2919,7 +2968,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
demoController?.replace(with: c)
}
controller.parentController()?.push(demoController)
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
return
}
@ -2951,7 +3004,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
)
controller.present(resultController, in: .window(.root))
} else {
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
}
}
)
@ -3302,6 +3358,24 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
}
})
self.longTapWithTabBar = { [weak self] in
guard let self else {
return
}
let _ = (context.engine.messages.attachMenuBots()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] attachMenuBots in
guard let self else {
return
}
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == self.botId && !$0.flags.contains(.notActivated) })
if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(self.source) {
self.removeAttachBot()
}
})
}
}
required public init(coder aDecoder: NSCoder) {
@ -3561,14 +3635,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
if let strongSelf = self {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(strongSelf.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
if let strongSelf = self {
let _ = context.engine.messages.removeBotFromAttachMenu(botId: strongSelf.botId).start()
strongSelf.dismiss()
}
})], parseMarkdown: true), in: .window(.root))
if let self {
self.removeAttachBot()
}
})))
}
@ -3580,6 +3648,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentInGlobalOverlay(contextController)
}
private func removeAttachBot() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start()
self.dismiss()
})], parseMarkdown: true), in: .window(.root))
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, controller: self)
@ -3660,7 +3739,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.controllerNode.webView?.setNeedsLayout()
}
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \(self.isMinimized ? "false" : "true")}")
let data: JSON = [
"is_visible": !self.isMinimized,
]
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: data.string)
}
}
}

View File

@ -1,5 +1,5 @@
{
"app": "11.10",
"app": "11.11",
"xcode": "16.2",
"bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff",
"macos": "15"