mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
ecb1711970
@ -1 +1,2 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
spm-files
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -73,3 +73,4 @@ buildServer.json
|
||||
.build/**
|
||||
Telegram.LSP.json
|
||||
**/.build/**
|
||||
spm-files
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -11,5 +11,8 @@
|
||||
},
|
||||
"search.exclude": {
|
||||
".git/**": true
|
||||
},
|
||||
"files.associations": {
|
||||
"memory": "cpp"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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";
|
||||
|
@ -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)?
|
||||
|
@ -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
|
447
build-system/bazel-utils/spm.bzl
Normal file
447
build-system/bazel-utils/spm.bzl
Normal 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]),
|
||||
},
|
||||
)
|
193
build-system/generate_spm.py
Normal file
193
build-system/generate_spm.py
Normal 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))
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
})
|
||||
})))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -1441,6 +1441,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
break
|
||||
case .messageLink:
|
||||
break
|
||||
case .stars:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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] = []
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)")
|
||||
}
|
||||
})
|
||||
|
@ -67,6 +67,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||
"//submodules/TelegramUI/Components/MediaAssetsContext",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramNotices",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_qrcode.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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>();
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "11.10",
|
||||
"app": "11.11",
|
||||
"xcode": "16.2",
|
||||
"bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff",
|
||||
"macos": "15"
|
||||
|
Loading…
x
Reference in New Issue
Block a user