Assorted updates and bug fixes
@ -184,6 +184,7 @@ swift_library(
|
||||
],
|
||||
deps = [
|
||||
"//submodules/TelegramUI:TelegramUI",
|
||||
"//third-party/boringssl:crypto",
|
||||
],
|
||||
)
|
||||
|
||||
@ -783,7 +784,7 @@ ios_framework(
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":SyncCoreFramework",
|
||||
":TelegramApiFramework",
|
||||
#":TelegramApiFramework",
|
||||
],
|
||||
minimum_os_version = "9.0",
|
||||
ipa_post_processor = strip_framework,
|
||||
@ -965,7 +966,7 @@ ios_framework(
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramApiFramework",
|
||||
#":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
":TelegramCoreFramework",
|
||||
":AsyncDisplayKitFramework",
|
||||
@ -1268,7 +1269,8 @@ ios_extension(
|
||||
frameworks = [
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramApiFramework",
|
||||
":TelegramCoreFramework",
|
||||
#":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
],
|
||||
)
|
||||
@ -1319,8 +1321,8 @@ ios_extension(
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
#":TelegramApiFramework",
|
||||
#":SyncCoreFramework",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1517,7 +1519,7 @@ ios_application(
|
||||
":MtProtoKitFramework",
|
||||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramApiFramework",
|
||||
#":TelegramApiFramework",
|
||||
":SyncCoreFramework",
|
||||
":TelegramCoreFramework",
|
||||
":AsyncDisplayKitFramework",
|
||||
|
@ -1,53 +0,0 @@
|
||||
APP_VERSION="1.0"
|
||||
CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
|
||||
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)
|
||||
|
||||
BAZEL=$(shell which bazel)
|
||||
|
||||
ifneq ($(BAZEL_CACHE_DIR),)
|
||||
export BAZEL_CACHE_FLAGS=\
|
||||
--disk_cache="${BAZEL_CACHE_DIR}"
|
||||
endif
|
||||
|
||||
BAZEL_COMMON_FLAGS=\
|
||||
--announce_rc \
|
||||
--features=swift.use_global_module_cache \
|
||||
|
||||
BAZEL_DEBUG_FLAGS=\
|
||||
--features=swift.enable_batch_mode \
|
||||
--swiftcopt=-j${CORE_COUNT_MINUS_ONE} \
|
||||
|
||||
BAZEL_OPT_FLAGS=\
|
||||
--swiftcopt=-whole-module-optimization \
|
||||
--swiftcopt='-num-threads' --swiftcopt='16' \
|
||||
|
||||
kill_xcode:
|
||||
killall Xcode || true
|
||||
|
||||
wallet_app_debug_arm64:
|
||||
WALLET_APP_VERSION="${APP_VERSION}" \
|
||||
build-system/prepare-build.sh Wallet distribution
|
||||
"${BAZEL}" build Wallet/Wallet ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} \
|
||||
-c dbg \
|
||||
--ios_multi_cpus=arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--verbose_failures
|
||||
|
||||
wallet_app:
|
||||
WALLET_APP_VERSION="${APP_VERSION}" \
|
||||
build-system/prepare-build.sh Wallet distribution
|
||||
"${BAZEL}" build Wallet/Wallet ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_OPT_FLAGS} \
|
||||
-c opt \
|
||||
--ios_multi_cpus=armv7,arm64 \
|
||||
--watchos_cpus=armv7k,arm64_32 \
|
||||
--verbose_failures
|
||||
|
||||
bazel_wallet_prepare_development_build:
|
||||
WALLET_APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
build-system/prepare-build.sh Wallet development
|
||||
|
||||
wallet_project: kill_xcode bazel_wallet_prepare_development_build
|
||||
WALLET_APP_VERSION="${APP_VERSION}" \
|
||||
BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \
|
||||
build-system/generate-xcode-project.sh Wallet
|
136
Wallet/BUCK
@ -1,136 +0,0 @@
|
||||
load("//Config:utils.bzl",
|
||||
"library_configs",
|
||||
)
|
||||
|
||||
load("//Config:wallet_configs.bzl",
|
||||
"app_binary_configs",
|
||||
"app_info_plist_substitutions",
|
||||
)
|
||||
|
||||
load("//Config:buck_rule_macros.bzl",
|
||||
"framework_binary_dependencies",
|
||||
"framework_bundle_dependencies",
|
||||
)
|
||||
|
||||
framework_dependencies = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
]
|
||||
|
||||
resource_dependencies = [
|
||||
"//submodules/WalletUI:WalletUIResources",
|
||||
"//submodules/WalletUI:WalletUIAssets",
|
||||
"//submodules/OverlayStatusController:OverlayStatusControllerResources",
|
||||
":StringResources",
|
||||
":InfoPlistStringResources",
|
||||
":Icons",
|
||||
":LaunchScreen",
|
||||
]
|
||||
|
||||
apple_resource(
|
||||
name = "StringResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"Strings/*.lproj/Localizable.strings",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "InfoPlistStringResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"InfoPlistStrings/*.lproj/InfoPlist.strings",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_asset_catalog(
|
||||
name = "Icons",
|
||||
dirs = [
|
||||
"Icons.xcassets",
|
||||
],
|
||||
app_icon = "AppIconWallet",
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "LaunchScreen",
|
||||
files = [
|
||||
"LaunchScreen.xib",
|
||||
],
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_library(
|
||||
name = "AppLibrary",
|
||||
visibility = [
|
||||
"//Wallet:...",
|
||||
],
|
||||
configs = library_configs(),
|
||||
swift_version = native.read_config("swift", "version"),
|
||||
srcs = glob([
|
||||
"Sources/**/*.m",
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
exported_linker_flags = [
|
||||
"-lc++",
|
||||
"-lz"
|
||||
],
|
||||
deps = [
|
||||
"//submodules/WalletUI:WalletUI",
|
||||
"//submodules/WalletCore:WalletCore",
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
]
|
||||
+ framework_binary_dependencies(framework_dependencies),
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/VideoToolbox.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/AVFoundation.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_binary(
|
||||
name = "AppBinary",
|
||||
visibility = [
|
||||
"//Wallet:...",
|
||||
],
|
||||
configs = app_binary_configs(),
|
||||
swift_version = native.read_config("swift", "version"),
|
||||
srcs = [
|
||||
"SupportFiles/Empty.swift",
|
||||
],
|
||||
deps = [
|
||||
":AppLibrary",
|
||||
]
|
||||
+ resource_dependencies,
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "Wallet",
|
||||
visibility = [
|
||||
"//:",
|
||||
],
|
||||
extension = "app",
|
||||
binary = ":AppBinary",
|
||||
product_name = "Wallet",
|
||||
info_plist = "Info.plist",
|
||||
info_plist_substitutions = app_info_plist_substitutions(),
|
||||
deps = [
|
||||
]
|
||||
+ framework_bundle_dependencies(framework_dependencies),
|
||||
)
|
||||
|
||||
apple_package(
|
||||
name = "AppPackage",
|
||||
bundle = ":Wallet",
|
||||
)
|
||||
|
||||
xcode_workspace_config(
|
||||
name = "workspace",
|
||||
workspace_name = "WalletWorkspace",
|
||||
src_target = ":Wallet",
|
||||
)
|
189
Wallet/BUILD
@ -1,189 +0,0 @@
|
||||
load("@build_bazel_rules_apple//apple:ios.bzl",
|
||||
"ios_application",
|
||||
"ios_extension",
|
||||
"ios_framework",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl",
|
||||
"swift_library",
|
||||
)
|
||||
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
|
||||
load(
|
||||
"//build-input/data:variables.bzl",
|
||||
"wallet_build_number",
|
||||
"wallet_version",
|
||||
"wallet_bundle_id",
|
||||
"wallet_team_id",
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "debug",
|
||||
values = {
|
||||
"compilation_mode": "dbg",
|
||||
},
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "empty",
|
||||
outs = ["empty.swift"],
|
||||
cmd = "touch $(OUTS)",
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "_LocalDebugOptions",
|
||||
srcs = [":empty"],
|
||||
copts = [
|
||||
"-Xfrontend",
|
||||
"-serialize-debugging-options",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
module_name = "_LocalDebugOptions",
|
||||
tags = ["no-remote"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
debug_deps = select({
|
||||
":debug": [":_LocalDebugOptions"],
|
||||
"//conditions:default": [],
|
||||
})
|
||||
|
||||
plist_fragment(
|
||||
name = "WalletInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{wallet_version}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{wallet_build_number}</string>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>TON Wallet</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{wallet_bundle_id}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TON Wallet</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Please allow TON Wallet access to your camera for scanning QR codes.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>For better security, please allow TON Wallet to use your Face ID to authenticate payments.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Please allow TON Wallet access to your Photo Stream in case you need to scan a QR code from a picture.</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<false/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresPersistentWiFi</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIViewEdgeAntialiasing</key>
|
||||
<false/>
|
||||
<key>UIViewGroupOpacity</key>
|
||||
<false/>
|
||||
""".format(
|
||||
wallet_version = wallet_version,
|
||||
wallet_build_number = wallet_build_number,
|
||||
wallet_bundle_id = wallet_bundle_id,
|
||||
)
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "Strings",
|
||||
srcs = glob([
|
||||
"Strings/**/*",
|
||||
], exclude = ["Strings/**/.*"]),
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "Icons",
|
||||
srcs = glob([
|
||||
"Icons.xcassets/**/*",
|
||||
], exclude = ["Icons.xcassets/**/.*"]),
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Main",
|
||||
srcs = [
|
||||
"Sources/main.m"
|
||||
],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "Lib",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
|
||||
],
|
||||
deps = [
|
||||
"//submodules/WalletUI:WalletUI",
|
||||
"//submodules/WalletCore:WalletCore",
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
],
|
||||
)
|
||||
|
||||
ios_application(
|
||||
name = "Wallet",
|
||||
bundle_id = wallet_bundle_id,
|
||||
families = ["iphone", "ipad"],
|
||||
minimum_os_version = "9.0",
|
||||
provisioning_profile = "//build-input/data/provisioning-profiles:Wallet.mobileprovision",
|
||||
infoplists = [
|
||||
":WalletInfoPlist.plist",
|
||||
],
|
||||
app_icons = [
|
||||
":Icons",
|
||||
],
|
||||
launch_storyboard = "LaunchScreen.xib",
|
||||
strings = [
|
||||
":Strings",
|
||||
],
|
||||
deps = [
|
||||
":Main",
|
||||
":Lib",
|
||||
],
|
||||
)
|
@ -1,119 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@40x40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@60x60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@58x58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@87x87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@80x80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@120x120.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@120x120-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "GramIcon@180x180.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@20x20.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@40x40-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@29x29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@58x58-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@40x40-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@80x80-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@76x76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@152x152.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "GramIcon@167x167.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "GramIcon-iTunesArtwork.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"pre-rendered" : true
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 523 B |
Before Width: | Height: | Size: 677 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.1 KiB |
@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIconWallet</string>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIconWallet</string>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Please allow TON Wallet access to your camera for scanning QR codes.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>For better security, please allow TON Wallet to use your Face ID to authenticate payments.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Please allow TON Wallet access to your Photo Stream in case you need to scan a QR code from a picture.</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<false/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresPersistentWiFi</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIViewEdgeAntialiasing</key>
|
||||
<false/>
|
||||
<key>UIViewGroupOpacity</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
@ -1,5 +0,0 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"NSCameraUsageDescription" = "Please allow TON Wallet access to your camera for scanning QR codes.";
|
||||
"NSPhotoLibraryUsageDescription" = "Please allow TON Wallet access to your Photo Stream in case you need to scan a QR code from a picture.";
|
||||
"NSFaceIDUsageDescription" = "For better security, please allow TON Wallet to use your Face ID to authenticate payments.";
|
@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15508"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<navigationController id="sSF-ws-R8Z">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translucent="NO" id="d5X-cy-bFB">
|
||||
<rect key="frame" x="0.0" y="-44" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="barTintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</navigationBar>
|
||||
<viewControllers>
|
||||
<viewController id="nui-yY-Mdn">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="YeQ-mV-ElX"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="n8M-Ll-9gB"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="iG4-qM-jMr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SrD-xF-5G3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="260"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="Wallet/SplashCornerL" translatesAutoresizingMaskIntoConstraints="NO" id="4bZ-PB-VJX">
|
||||
<rect key="frame" x="0.0" y="260" width="10" height="10"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="Wallet/SplashCornerR" translatesAutoresizingMaskIntoConstraints="NO" id="K7L-Vs-8lO">
|
||||
<rect key="frame" x="404" y="260" width="10" height="10"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="riQ-lf-mYe"/>
|
||||
</viewController>
|
||||
</viewControllers>
|
||||
<point key="canvasLocation" x="-137.68115942028987" y="-95.089285714285708"/>
|
||||
</navigationController>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="Wallet/SplashCornerL" width="10" height="10"/>
|
||||
<image name="Wallet/SplashCornerR" width="10" height="10"/>
|
||||
</resources>
|
||||
</document>
|
@ -1,27 +0,0 @@
|
||||
# Test Gram Wallet (iOS)
|
||||
|
||||
This is the source code and build instructions for a TON Testnet Wallet implementation for iOS.
|
||||
|
||||
1. Install Xcode 11.4
|
||||
```
|
||||
https://apps.apple.com/app/xcode/id497799835
|
||||
```
|
||||
|
||||
Make sure to launch Xcode at least once and set up command-line tools paths (Xcode — Preferences — Locations — Command Line Tools)
|
||||
|
||||
2. Build the app (IPA)
|
||||
|
||||
Note:
|
||||
It is recommended to use an artifact cache to optimize build speed. Prepend any of the following commands with
|
||||
```
|
||||
BAZEL_CACHE_DIR="path/to/existing/directory"
|
||||
```
|
||||
|
||||
```
|
||||
sh wallet_env.sh make wallet_app
|
||||
```
|
||||
|
||||
3. If needed, generate Xcode project
|
||||
```
|
||||
sh wallet_env.sh make wallet_project
|
||||
```
|
@ -1,848 +0,0 @@
|
||||
import UIKit
|
||||
import Display
|
||||
import OverlayStatusController
|
||||
import SwiftSignalKit
|
||||
import BuildConfig
|
||||
import WalletUI
|
||||
import WalletCore
|
||||
import AVFoundation
|
||||
|
||||
private func encodeText(_ string: String, _ key: Int) -> String {
|
||||
var result = ""
|
||||
for c in string.unicodeScalars {
|
||||
result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private let statusBarRootViewClass: AnyClass = NSClassFromString("UIStatusBar")!
|
||||
private let statusBarPlaceholderClass: AnyClass? = NSClassFromString("UIStatusBar_Placeholder")
|
||||
private let cutoutStatusBarForegroundClass: AnyClass? = NSClassFromString("_UIStatusBar")
|
||||
private let keyboardViewClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuIptuWjfx", -1))!
|
||||
private let keyboardViewContainerClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuDpoubjofsWjfx", -1))!
|
||||
|
||||
private let keyboardWindowClass: AnyClass? = {
|
||||
if #available(iOS 9.0, *) {
|
||||
return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1))
|
||||
} else {
|
||||
return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1))
|
||||
}
|
||||
}()
|
||||
|
||||
private class ApplicationStatusBarHost: StatusBarHost {
|
||||
private let application = UIApplication.shared
|
||||
|
||||
var isApplicationInForeground: Bool {
|
||||
switch self.application.applicationState {
|
||||
case .background:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var statusBarFrame: CGRect {
|
||||
return self.application.statusBarFrame
|
||||
}
|
||||
var statusBarStyle: UIStatusBarStyle {
|
||||
get {
|
||||
return self.application.statusBarStyle
|
||||
} set(value) {
|
||||
self.setStatusBarStyle(value, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) {
|
||||
self.application.setStatusBarStyle(style, animated: animated)
|
||||
}
|
||||
|
||||
func setStatusBarHidden(_ value: Bool, animated: Bool) {
|
||||
self.application.setStatusBarHidden(value, with: animated ? .fade : .none)
|
||||
}
|
||||
|
||||
var statusBarWindow: UIView? {
|
||||
return self.application.value(forKey: "statusBarWindow") as? UIView
|
||||
}
|
||||
|
||||
var statusBarView: UIView? {
|
||||
guard let containerView = self.statusBarWindow?.subviews.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if containerView.isKind(of: statusBarRootViewClass) {
|
||||
return containerView
|
||||
}
|
||||
if let statusBarPlaceholderClass = statusBarPlaceholderClass {
|
||||
if containerView.isKind(of: statusBarPlaceholderClass) {
|
||||
return containerView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for subview in containerView.subviews {
|
||||
if let cutoutStatusBarForegroundClass = cutoutStatusBarForegroundClass, subview.isKind(of: cutoutStatusBarForegroundClass) {
|
||||
return subview
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var keyboardWindow: UIWindow? {
|
||||
guard let keyboardWindowClass = keyboardWindowClass else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for window in UIApplication.shared.windows {
|
||||
if window.isKind(of: keyboardWindowClass) {
|
||||
return window
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var keyboardView: UIView? {
|
||||
guard let keyboardWindow = self.keyboardWindow, let keyboardViewContainerClass = keyboardViewContainerClass, let keyboardViewClass = keyboardViewClass else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for view in keyboardWindow.subviews {
|
||||
if view.isKind(of: keyboardViewContainerClass) {
|
||||
for subview in view.subviews {
|
||||
if subview.isKind(of: keyboardViewClass) {
|
||||
return subview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var handleVolumeControl: Signal<Bool, NoError> {
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
|
||||
private final class FileBackedStorageImpl {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private var data: Data?
|
||||
private var subscribers = Bag<(Data?) -> Void>()
|
||||
|
||||
init(queue: Queue, path: String) {
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
}
|
||||
|
||||
func get() -> Data? {
|
||||
if let data = self.data {
|
||||
return data
|
||||
} else {
|
||||
self.data = try? Data(contentsOf: URL(fileURLWithPath: self.path))
|
||||
return self.data
|
||||
}
|
||||
}
|
||||
|
||||
func set(data: Data) {
|
||||
self.data = data
|
||||
do {
|
||||
try data.write(to: URL(fileURLWithPath: self.path), options: .atomic)
|
||||
} catch let error {
|
||||
print("Error writng data: \(error)")
|
||||
}
|
||||
for f in self.subscribers.copyItems() {
|
||||
f(data)
|
||||
}
|
||||
}
|
||||
|
||||
func watch(_ f: @escaping (Data?) -> Void) -> Disposable {
|
||||
f(self.get())
|
||||
let index = self.subscribers.add(f)
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.subscribers.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class FileBackedStorage {
|
||||
private let queue = Queue()
|
||||
private let impl: QueueLocalObject<FileBackedStorageImpl>
|
||||
|
||||
init(path: String) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return FileBackedStorageImpl(queue: queue, path: path)
|
||||
})
|
||||
}
|
||||
|
||||
func get() -> Signal<Data?, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.impl.with { impl in
|
||||
subscriber.putNext(impl.get())
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
func set(data: Data) -> Signal<Never, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.impl.with { impl in
|
||||
impl.set(data: data)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
func update<T>(_ f: @escaping (Data?) -> (Data, T)) -> Signal<T, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.impl.with { impl in
|
||||
let (data, result) = f(impl.get())
|
||||
impl.set(data: data)
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
func watch() -> Signal<Data?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.watch({ data in
|
||||
subscriber.putNext(data)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let records = Atomic<[WalletStateRecord]>(value: [])
|
||||
|
||||
private final class WalletStorageInterfaceImpl: WalletStorageInterface {
|
||||
private let storage: FileBackedStorage
|
||||
private let configurationStorage: FileBackedStorage
|
||||
|
||||
init(path: String, configurationPath: String) {
|
||||
self.storage = FileBackedStorage(path: path)
|
||||
self.configurationStorage = FileBackedStorage(path: configurationPath)
|
||||
}
|
||||
|
||||
func watchWalletRecords() -> Signal<[WalletStateRecord], NoError> {
|
||||
return self.storage.watch()
|
||||
|> map { data -> [WalletStateRecord] in
|
||||
guard let data = data else {
|
||||
return []
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(Array<WalletStateRecord>.self, from: data)
|
||||
} catch let error {
|
||||
print("Error deserializing data: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getWalletRecords() -> Signal<[WalletStateRecord], NoError> {
|
||||
return self.storage.get()
|
||||
|> map { data -> [WalletStateRecord] in
|
||||
guard let data = data else {
|
||||
return []
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(Array<WalletStateRecord>.self, from: data)
|
||||
} catch let error {
|
||||
print("Error deserializing data: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateWalletRecords(_ f: @escaping ([WalletStateRecord]) -> [WalletStateRecord]) -> Signal<[WalletStateRecord], NoError> {
|
||||
return self.storage.update { data -> (Data, [WalletStateRecord]) in
|
||||
let records: [WalletStateRecord] = data.flatMap {
|
||||
try? JSONDecoder().decode(Array<WalletStateRecord>.self, from: $0)
|
||||
} ?? []
|
||||
let updatedRecords = f(records)
|
||||
do {
|
||||
let updatedData = try JSONEncoder().encode(updatedRecords)
|
||||
return (updatedData, updatedRecords)
|
||||
} catch let error {
|
||||
print("Error serializing data: \(error)")
|
||||
return (Data(), updatedRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergedLocalWalletConfiguration() -> Signal<MergedLocalWalletConfiguration, NoError> {
|
||||
return self.configurationStorage.watch()
|
||||
|> map { data -> MergedLocalWalletConfiguration in
|
||||
guard let data = data, !data.isEmpty else {
|
||||
return .default
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(MergedLocalWalletConfiguration.self, from: data)
|
||||
} catch let error {
|
||||
print("Error deserializing data: \(error)")
|
||||
return .default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func localWalletConfiguration() -> Signal<LocalWalletConfiguration, NoError> {
|
||||
return self.mergedLocalWalletConfiguration()
|
||||
|> mapToSignal { value -> Signal<LocalWalletConfiguration, NoError> in
|
||||
return .single(value.configuration)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
func updateMergedLocalWalletConfiguration(_ f: @escaping (MergedLocalWalletConfiguration) -> MergedLocalWalletConfiguration) -> Signal<Never, NoError> {
|
||||
return self.configurationStorage.update { data -> (Data, Void) in
|
||||
do {
|
||||
let current: MergedLocalWalletConfiguration?
|
||||
if let data = data, !data.isEmpty {
|
||||
current = try? JSONDecoder().decode(MergedLocalWalletConfiguration.self, from: data)
|
||||
} else {
|
||||
current = nil
|
||||
}
|
||||
let updated = f(current ?? .default)
|
||||
let updatedData = try JSONEncoder().encode(updated)
|
||||
return (updatedData, Void())
|
||||
} catch let error {
|
||||
print("Error serializing data: \(error)")
|
||||
return (Data(), Void())
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
func updateLocalWalletConfiguration(_ f: @escaping (LocalWalletConfiguration) -> LocalWalletConfiguration) -> Signal<Never, NoError> {
|
||||
return self.updateMergedLocalWalletConfiguration { value in
|
||||
var value = value
|
||||
value.configuration = f(value.configuration)
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WalletContextImpl: NSObject, WalletContext, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
var storage: WalletStorageInterface {
|
||||
return self.storageImpl
|
||||
}
|
||||
private let storageImpl: WalletStorageInterfaceImpl
|
||||
let tonInstance: TonInstance
|
||||
let keychain: TonKeychain
|
||||
let presentationData: WalletPresentationData
|
||||
let window: Window1
|
||||
|
||||
let supportsCustomConfigurations: Bool = true
|
||||
let termsUrl: String? = nil
|
||||
let feeInfoUrl: String? = nil
|
||||
|
||||
private var currentImagePickerCompletion: ((UIImage) -> Void)?
|
||||
|
||||
var inForeground: Signal<Bool, NoError> {
|
||||
return .single(true)
|
||||
}
|
||||
|
||||
func getServerSalt() -> Signal<Data, WalletContextGetServerSaltError> {
|
||||
return .single(Data())
|
||||
}
|
||||
|
||||
func downloadFile(url: URL) -> Signal<Data, WalletDownloadFileError> {
|
||||
return download(url: url)
|
||||
|> mapError { _ in
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|
||||
func updateResolvedWalletConfiguration(source: LocalWalletConfigurationSource, blockchainName: String, resolvedValue: String) -> Signal<Never, NoError> {
|
||||
return self.storageImpl.updateMergedLocalWalletConfiguration { configuration in
|
||||
var configuration = configuration
|
||||
configuration.configuration.source = source
|
||||
configuration.configuration.blockchainName = blockchainName
|
||||
configuration.resolved = ResolvedLocalWalletConfiguration(source: source, value: resolvedValue)
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
func presentNativeController(_ controller: UIViewController) {
|
||||
self.window.presentNative(controller)
|
||||
}
|
||||
|
||||
func idleTimerExtension() -> Disposable {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
func openUrl(_ url: String) {
|
||||
if let parsedUrl = URL(string: url) {
|
||||
UIApplication.shared.openURL(parsedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
func shareUrl(_ url: String) {
|
||||
if let parsedUrl = URL(string: url) {
|
||||
self.presentNativeController(UIActivityViewController(activityItems: [parsedUrl], applicationActivities: nil))
|
||||
}
|
||||
}
|
||||
|
||||
func openPlatformSettings() {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeAccessToCamera(completion: @escaping () -> Void) {
|
||||
AVCaptureDevice.requestAccess(for: AVMediaType.video) { [weak self] response in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if response {
|
||||
completion()
|
||||
} else {
|
||||
let presentationData = strongSelf.presentationData
|
||||
let controller = standardTextAlertController(theme: presentationData.theme.alert, title: presentationData.strings.Wallet_AccessDenied_Title, text: presentationData.strings.Wallet_AccessDenied_Camera, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Intro_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Wallet_AccessDenied_Settings, action: {
|
||||
strongSelf.openPlatformSettings()
|
||||
})])
|
||||
strongSelf.window.present(controller, on: .root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pickImage(present: @escaping (ViewController) -> Void, completion: @escaping (UIImage) -> Void) {
|
||||
self.currentImagePickerCompletion = completion
|
||||
|
||||
let pickerController = UIImagePickerController()
|
||||
pickerController.delegate = self
|
||||
pickerController.allowsEditing = false
|
||||
pickerController.mediaTypes = ["public.image"]
|
||||
pickerController.sourceType = .photoLibrary
|
||||
self.presentNativeController(pickerController)
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
let currentImagePickerCompletion = self.currentImagePickerCompletion
|
||||
self.currentImagePickerCompletion = nil
|
||||
if let image = info[.editedImage] as? UIImage {
|
||||
currentImagePickerCompletion?(image)
|
||||
} else if let image = info[.originalImage] as? UIImage {
|
||||
currentImagePickerCompletion?(image)
|
||||
}
|
||||
picker.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
self.currentImagePickerCompletion = nil
|
||||
picker.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
init(basePath: String, storage: WalletStorageInterfaceImpl, config: String, blockchainName: String, presentationData: WalletPresentationData, navigationBarTheme: NavigationBarTheme, window: Window1) {
|
||||
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: basePath + "/keys"), withIntermediateDirectories: true, attributes: nil)
|
||||
self.storageImpl = storage
|
||||
|
||||
self.window = window
|
||||
|
||||
self.tonInstance = TonInstance(
|
||||
basePath: basePath + "/keys",
|
||||
config: config,
|
||||
blockchainName: blockchainName,
|
||||
proxy: nil
|
||||
)
|
||||
|
||||
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
self.keychain = TonKeychain(encryptionPublicKey: {
|
||||
return .single(Data())
|
||||
}, encrypt: { data in
|
||||
return .single(TonKeychainEncryptedData(publicKey: Data(), data: data))
|
||||
}, decrypt: { data in
|
||||
return .single(data.data)
|
||||
})
|
||||
#else
|
||||
self.keychain = TonKeychain(encryptionPublicKey: {
|
||||
return Signal { subscriber in
|
||||
BuildConfig.getHardwareEncryptionAvailable(withBaseAppBundleId: baseAppBundleId, completion: { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, encrypt: { data in
|
||||
return Signal { subscriber in
|
||||
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result, publicKey in
|
||||
if let result = result, let publicKey = publicKey {
|
||||
subscriber.putNext(TonKeychainEncryptedData(publicKey: publicKey, data: result))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, decrypt: { encryptedData in
|
||||
return Signal { subscriber in
|
||||
BuildConfig.decryptApplicationSecret(encryptedData.data, publicKey: encryptedData.publicKey, baseAppBundleId: baseAppBundleId, completion: { result, cancelled in
|
||||
if let result = result {
|
||||
subscriber.putNext(result)
|
||||
} else {
|
||||
let error: TonKeychainDecryptDataError
|
||||
if cancelled {
|
||||
error = .cancelled
|
||||
} else {
|
||||
error = .generic
|
||||
}
|
||||
subscriber.putError(error)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
})
|
||||
#endif
|
||||
|
||||
self.presentationData = presentationData
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppDelegate)
|
||||
final class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
private var mainWindow: Window1?
|
||||
private var walletContext: WalletContextImpl?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
let statusBarHost = ApplicationStatusBarHost()
|
||||
let (window, hostView) = nativeWindowHostView()
|
||||
let mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost)
|
||||
self.mainWindow = mainWindow
|
||||
hostView.containerView.backgroundColor = UIColor.white
|
||||
self.window = window
|
||||
|
||||
let accentColor = UIColor(rgb: 0x007ee5)
|
||||
|
||||
let navigationBarTheme = NavigationBarTheme(
|
||||
buttonColor: accentColor,
|
||||
disabledButtonColor: UIColor(rgb: 0xd0d0d0),
|
||||
primaryTextColor: .black,
|
||||
backgroundColor: UIColor(rgb: 0xf7f7f7),
|
||||
separatorColor: UIColor(rgb: 0xb1b1b1),
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3b30),
|
||||
badgeStrokeColor: UIColor(rgb: 0xff3b30),
|
||||
badgeTextColor: .white
|
||||
)
|
||||
|
||||
let presentationData = WalletPresentationData(
|
||||
theme: WalletTheme(
|
||||
info: WalletInfoTheme(
|
||||
buttonBackgroundColor: UIColor(rgb: 0x32aafe),
|
||||
buttonTextColor: .white,
|
||||
incomingFundsTitleColor: UIColor(rgb: 0x00b12c),
|
||||
outgoingFundsTitleColor: UIColor(rgb: 0xff3b30)
|
||||
), transaction: WalletTransactionTheme(
|
||||
descriptionBackgroundColor: UIColor(rgb: 0xf1f1f4),
|
||||
descriptionTextColor: .black
|
||||
), setup: WalletSetupTheme(
|
||||
buttonFillColor: accentColor,
|
||||
buttonForegroundColor: .white,
|
||||
inputBackgroundColor: UIColor(rgb: 0xe9e9e9),
|
||||
inputPlaceholderColor: UIColor(rgb: 0x818086),
|
||||
inputTextColor: .black,
|
||||
inputClearButtonColor: UIColor(rgb: 0x7b7b81).withAlphaComponent(0.8)
|
||||
),
|
||||
list: WalletListTheme(
|
||||
itemPrimaryTextColor: .black,
|
||||
itemSecondaryTextColor: UIColor(rgb: 0x8e8e93),
|
||||
itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce),
|
||||
itemDestructiveColor: UIColor(rgb: 0xff3b30),
|
||||
itemAccentColor: accentColor,
|
||||
itemDisabledTextColor: UIColor(rgb: 0x8e8e93),
|
||||
plainBackgroundColor: .white,
|
||||
blocksBackgroundColor: UIColor(rgb: 0xefeff4),
|
||||
itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc),
|
||||
itemBlocksBackgroundColor: .white,
|
||||
itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc),
|
||||
itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea),
|
||||
sectionHeaderTextColor: UIColor(rgb: 0x6d6d72),
|
||||
freeTextColor: UIColor(rgb: 0x6d6d72),
|
||||
freeTextErrorColor: UIColor(rgb: 0xcf3030),
|
||||
inputClearButtonColor: UIColor(rgb: 0xcccccc)
|
||||
),
|
||||
statusBarStyle: .Black,
|
||||
navigationBar: navigationBarTheme,
|
||||
keyboardAppearance: .light,
|
||||
alert: AlertControllerTheme(
|
||||
backgroundType: .light,
|
||||
backgroundColor: .white,
|
||||
separatorColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
highlightedItemColor: UIColor(rgb: 0xe5e5ea),
|
||||
primaryColor: .black,
|
||||
secondaryColor: UIColor(rgb: 0x5e5e5e),
|
||||
accentColor: accentColor,
|
||||
destructiveColor: UIColor(rgb: 0xff3b30),
|
||||
disabledColor: UIColor(rgb: 0xd0d0d0),
|
||||
baseFontSize: 17.0
|
||||
),
|
||||
actionSheet: ActionSheetControllerTheme(
|
||||
dimColor: UIColor(white: 0.0, alpha: 0.4),
|
||||
backgroundType: .light,
|
||||
itemBackgroundColor: .white,
|
||||
itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
standardActionTextColor: accentColor,
|
||||
destructiveActionTextColor: UIColor(rgb: 0xff3b30),
|
||||
disabledActionTextColor: UIColor(rgb: 0xb3b3b3),
|
||||
primaryTextColor: .black,
|
||||
secondaryTextColor: UIColor(rgb: 0x5e5e5e),
|
||||
controlAccentColor: accentColor,
|
||||
controlColor: UIColor(rgb: 0x7e8791),
|
||||
switchFrameColor: UIColor(rgb: 0xe0e0e0),
|
||||
switchContentColor: UIColor(rgb: 0x77d572),
|
||||
switchHandleColor: UIColor(rgb: 0xffffff),
|
||||
baseFontSize: 17.0
|
||||
)
|
||||
), strings: WalletStrings(
|
||||
primaryComponent: WalletStringsComponent(
|
||||
languageCode: "en",
|
||||
localizedName: "English",
|
||||
pluralizationRulesCode: "en",
|
||||
dict: [:]
|
||||
),
|
||||
secondaryComponent: nil,
|
||||
groupingSeparator: " "
|
||||
), dateTimeFormat: WalletPresentationDateTimeFormat(
|
||||
timeFormat: .regular,
|
||||
dateFormat: .dayFirst,
|
||||
dateSeparator: ".",
|
||||
decimalSeparator: ".",
|
||||
groupingSeparator: " "
|
||||
)
|
||||
)
|
||||
|
||||
let navigationController = NavigationController(
|
||||
mode: .single,
|
||||
theme: NavigationControllerTheme(
|
||||
statusBar: .black,
|
||||
navigationBar: navigationBarTheme,
|
||||
emptyAreaColor: .white
|
||||
), backgroundDetailsMode: nil
|
||||
)
|
||||
|
||||
mainWindow.viewController = navigationController
|
||||
|
||||
navigationController.setViewControllers([WalletApplicationSplashScreen(theme: presentationData.theme)], animated: false)
|
||||
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
||||
#if DEBUG
|
||||
print("Starting with \(documentsPath)")
|
||||
#endif
|
||||
|
||||
let storage = WalletStorageInterfaceImpl(path: documentsPath + "/data", configurationPath: documentsPath + "/configuration_v2")
|
||||
|
||||
let initialConfigValue = storage.mergedLocalWalletConfiguration()
|
||||
|> take(1)
|
||||
|> mapToSignal { configuration -> Signal<(ResolvedLocalWalletConfiguration, String), NoError> in
|
||||
if let resolved = configuration.resolved, resolved.source == configuration.configuration.source {
|
||||
return .single((resolved, configuration.configuration.blockchainName))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
let updatedConfigValue = storage.localWalletConfiguration()
|
||||
|> mapToSignal { configuration -> Signal<(ResolvedLocalWalletConfiguration, String), NoError> in
|
||||
switch configuration.source {
|
||||
case let .url(url):
|
||||
guard let parsedUrl = URL(string: url) else {
|
||||
return .complete()
|
||||
}
|
||||
return download(url: parsedUrl)
|
||||
|> retry(1.0, maxDelay: 5.0, onQueue: .mainQueue())
|
||||
|> mapToSignal { data -> Signal<(ResolvedLocalWalletConfiguration, String), NoError> in
|
||||
if let string = String(data: data, encoding: .utf8) {
|
||||
return .single((ResolvedLocalWalletConfiguration(source: configuration.source, value: string), configuration.blockchainName))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .string(string):
|
||||
return .single((ResolvedLocalWalletConfiguration(source: configuration.source, value: string), configuration.blockchainName))
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
return lhs.0 == rhs.0 && lhs.1 == rhs.1
|
||||
})
|
||||
|> afterNext { (resolved, _) in
|
||||
let _ = storage.updateMergedLocalWalletConfiguration({ current in
|
||||
var current = current
|
||||
current.resolved = resolved
|
||||
return current
|
||||
}).start()
|
||||
}
|
||||
|
||||
let resolvedInitialConfig = (
|
||||
initialConfigValue
|
||||
|> then(updatedConfigValue)
|
||||
)
|
||||
|> take(1)
|
||||
|
||||
let _ = (resolvedInitialConfig
|
||||
|> deliverOnMainQueue).start(next: { (initialResolvedConfig, initialConfigBlockchainName) in
|
||||
let walletContext = WalletContextImpl(basePath: documentsPath, storage: storage, config: initialResolvedConfig.value, blockchainName: initialConfigBlockchainName, presentationData: presentationData, navigationBarTheme: navigationBarTheme, window: mainWindow)
|
||||
self.walletContext = walletContext
|
||||
|
||||
let beginWithController: (ViewController) -> Void = { controller in
|
||||
let begin: (Bool) -> Void = { animated in
|
||||
navigationController.setViewControllers([controller], animated: false)
|
||||
if animated {
|
||||
navigationController.viewControllers.last?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
||||
var previousBlockchainName = initialConfigBlockchainName
|
||||
|
||||
let _ = (updatedConfigValue
|
||||
|> deliverOnMainQueue).start(next: { resolved, blockchainName in
|
||||
let _ = walletContext.tonInstance.validateConfig(config: resolved.value, blockchainName: blockchainName).start(error: { _ in
|
||||
}, completed: {
|
||||
let _ = walletContext.tonInstance.updateConfig(config: resolved.value, blockchainName: blockchainName).start()
|
||||
|
||||
if previousBlockchainName != blockchainName {
|
||||
previousBlockchainName = blockchainName
|
||||
|
||||
let overlayController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
mainWindow.present(overlayController, on: .root)
|
||||
|
||||
let _ = (deleteAllLocalWalletsData(storage: walletContext.storage, tonInstance: walletContext.tonInstance)
|
||||
|> deliverOnMainQueue).start(error: { [weak overlayController] _ in
|
||||
overlayController?.dismiss()
|
||||
}, completed: { [weak overlayController] in
|
||||
overlayController?.dismiss()
|
||||
|
||||
navigationController.setViewControllers([WalletSplashScreen(context: walletContext, mode: .intro, walletCreatedPreloadState: nil)], animated: true)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if let splashScreen = navigationController.viewControllers.first as? WalletApplicationSplashScreen, let _ = controller as? WalletSplashScreen {
|
||||
splashScreen.animateOut(completion: {
|
||||
begin(true)
|
||||
})
|
||||
} else {
|
||||
begin(false)
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (combineLatest(queue: .mainQueue(),
|
||||
walletContext.storage.getWalletRecords(),
|
||||
walletContext.keychain.encryptionPublicKey()
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { records, publicKey in
|
||||
if let record = records.first {
|
||||
if let publicKey = publicKey {
|
||||
let recordPublicKey: Data
|
||||
switch record.info {
|
||||
case let .ready(info, _, _):
|
||||
recordPublicKey = info.encryptedSecret.publicKey
|
||||
case let .imported(info):
|
||||
recordPublicKey = info.encryptedSecret.publicKey
|
||||
}
|
||||
if recordPublicKey == publicKey {
|
||||
switch record.info {
|
||||
case let .ready(info, exportCompleted, _):
|
||||
if exportCompleted {
|
||||
let _ = (walletAddress(walletInfo: info, tonInstance: walletContext.tonInstance)
|
||||
|> deliverOnMainQueue).start(next: { address in
|
||||
let infoScreen = WalletInfoScreen(context: walletContext, walletInfo: info, address: address, enableDebugActions: false)
|
||||
beginWithController(infoScreen)
|
||||
})
|
||||
} else {
|
||||
let createdScreen = WalletSplashScreen(context: walletContext, mode: .created(walletInfo: info, words: nil), walletCreatedPreloadState: nil)
|
||||
beginWithController(createdScreen)
|
||||
}
|
||||
case let .imported(info):
|
||||
let createdScreen = WalletSplashScreen(context: walletContext, mode: .successfullyImported(importedInfo: info), walletCreatedPreloadState: nil)
|
||||
beginWithController(createdScreen)
|
||||
}
|
||||
} else {
|
||||
let splashScreen = WalletSplashScreen(context: walletContext, mode: .secureStorageReset(.changed), walletCreatedPreloadState: nil)
|
||||
beginWithController(splashScreen)
|
||||
}
|
||||
} else {
|
||||
let splashScreen = WalletSplashScreen(context: walletContext, mode: WalletSplashMode.secureStorageReset(.notAvailable), walletCreatedPreloadState: nil)
|
||||
beginWithController(splashScreen)
|
||||
}
|
||||
} else {
|
||||
if publicKey != nil {
|
||||
let splashScreen = WalletSplashScreen(context: walletContext, mode: .intro, walletCreatedPreloadState: nil)
|
||||
beginWithController(splashScreen)
|
||||
} else {
|
||||
let splashScreen = WalletSplashScreen(context: walletContext, mode: .secureStorageNotAvailable, walletCreatedPreloadState: nil)
|
||||
beginWithController(splashScreen)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private enum DownloadFileError {
|
||||
case network
|
||||
}
|
||||
|
||||
private func download(url: URL) -> Signal<Data, DownloadFileError> {
|
||||
return Signal { subscriber in
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, _, error in
|
||||
let _ = completed.swap(true)
|
||||
if let location = location, let data = try? Data(contentsOf: location) {
|
||||
subscriber.putNext(data)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.network)
|
||||
}
|
||||
})
|
||||
downloadTask.resume()
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
downloadTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolvedLocalWalletConfiguration: Codable, Equatable {
|
||||
var source: LocalWalletConfigurationSource
|
||||
var value: String
|
||||
}
|
||||
|
||||
struct MergedLocalWalletConfiguration: Codable, Equatable {
|
||||
var configuration: LocalWalletConfiguration
|
||||
var resolved: ResolvedLocalWalletConfiguration?
|
||||
}
|
||||
|
||||
private extension MergedLocalWalletConfiguration {
|
||||
static var `default`: MergedLocalWalletConfiguration {
|
||||
return MergedLocalWalletConfiguration(configuration: LocalWalletConfiguration(source: .url("https://test.ton.org/config.json"), blockchainName: "testnet2"), resolved: nil)
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
@objc(Application) class Application: UIApplication {
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, @"Application", @"AppDelegate");
|
||||
}
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
"Wallet.Updated.JustNow" = "updated just now";
|
||||
"Wallet.Updated.MinutesAgo_0" = "%@ minutes ago"; //three to ten
|
||||
"Wallet.Updated.MinutesAgo_1" = "1 minute ago"; //one
|
||||
"Wallet.Updated.MinutesAgo_2" = "2 minutes ago"; //two
|
||||
"Wallet.Updated.MinutesAgo_3_10" = "%@ minutes ago"; //three to ten
|
||||
"Wallet.Updated.MinutesAgo_many" = "%@ minutes ago"; // more than ten
|
||||
"Wallet.Updated.MinutesAgo_any" = "%@ minutes ago"; // more than ten
|
||||
"Wallet.Updated.HoursAgo_0" = "%@ hours ago";
|
||||
"Wallet.Updated.HoursAgo_1" = "1 hour ago";
|
||||
"Wallet.Updated.HoursAgo_2" = "2 hours ago";
|
||||
"Wallet.Updated.HoursAgo_3_10" = "%@ hours ago";
|
||||
"Wallet.Updated.HoursAgo_any" = "%@ hours ago";
|
||||
"Wallet.Updated.HoursAgo_many" = "%@ hours ago";
|
||||
"Wallet.Updated.HoursAgo_0" = "%@ hours ago";
|
||||
"Wallet.Updated.YesterdayAt" = "yesterday at %@";
|
||||
"Wallet.Updated.AtDate" = "%@";
|
||||
"Wallet.Updated.TodayAt" = "today at %@";
|
||||
"Wallet.Info.WalletCreated" = "Wallet Created";
|
||||
"Wallet.Info.Address" = "Your wallet address";
|
||||
"Wallet.Info.YourBalance" = "your balance";
|
||||
"Wallet.Info.Receive" = "Receive";
|
||||
"Wallet.Info.ReceiveGrams" = "Receive Grams";
|
||||
"Wallet.Info.Send" = "Send";
|
||||
"Wallet.Info.RefreshErrorTitle" = "No network";
|
||||
"Wallet.Info.RefreshErrorText" = "Couldn't refresh balance. Please make sure your internet connection is working and try again.";
|
||||
"Wallet.Info.RefreshErrorNetworkText" = "Wallet state can not be retrieved at this time. Please try again later.";
|
||||
"Wallet.Info.UnknownTransaction" = "Empty Transaction";
|
||||
"Wallet.Info.TransactionTo" = "to";
|
||||
"Wallet.Info.TransactionFrom" = "from";
|
||||
"Wallet.Info.Updating" = "updating";
|
||||
"Wallet.Info.TransactionBlockchainFee" = "%@ blockchain fees";
|
||||
"Wallet.Info.TransactionPendingHeader" = "Pending";
|
||||
"Wallet.Qr.ScanCode" = "Scan QR Code";
|
||||
"Wallet.Qr.Title" = "QR Code";
|
||||
"Wallet.Receive.Title" = "Receive Grams";
|
||||
"Wallet.Receive.AddressHeader" = "YOUR WALLET ADDRESS";
|
||||
"Wallet.Receive.InvoiceUrlHeader" = "INVOICE URL";
|
||||
"Wallet.Receive.CopyAddress" = "Copy Wallet Address";
|
||||
"Wallet.Receive.CopyInvoiceUrl" = "Copy Invoice URL";
|
||||
"Wallet.Receive.ShareAddress" = "Share Wallet Address";
|
||||
"Wallet.Receive.ShareInvoiceUrl" = "Share Invoice URL";
|
||||
"Wallet.Receive.ShareUrlInfo" = "Share this link with other Gram wallet owners to receive Grams from them. Note: this link won't work for real Grams.";
|
||||
"Wallet.Receive.AmountHeader" = "AMOUNT";
|
||||
"Wallet.Receive.AmountText" = "Grams to receive";
|
||||
"Wallet.Receive.AmountInfo" = "You can specify the amount and purpose of the payment to save the sender some time.";
|
||||
"Wallet.Receive.CommentHeader" = "COMMENT (OPTIONAL)";
|
||||
"Wallet.Receive.CommentInfo" = "Description of the payment";
|
||||
"Wallet.Receive.AddressCopied" = "Address copied to clipboard.";
|
||||
"Wallet.Receive.InvoiceUrlCopied" = "Invoice URL copied to clipboard.";
|
||||
"Wallet.Send.Title" = "Send Grams";
|
||||
"Wallet.Send.AddressHeader" = "RECIPIENT WALLET ADDRESS";
|
||||
"Wallet.Send.AddressText" = "Enter wallet address...";
|
||||
"Wallet.Send.AddressInfo" = "Paste the 48-letter address of the recipient here or ask them to send you a ton:// link.";
|
||||
"Wallet.Send.Balance" = "Balance: %@";
|
||||
"Wallet.Send.AmountText" = "Grams to send";
|
||||
"Wallet.Send.Confirmation" = "Confirmation";
|
||||
"Wallet.Send.ConfirmationText" = "Do you want to send **%1$@** Grams to\n\n%2$@?\n\nBlockchain fees: ~%3$@ grams";
|
||||
"Wallet.Send.ConfirmationConfirm" = "Confirm";
|
||||
"Wallet.Send.Send" = "Send";
|
||||
"Wallet.Send.OwnAddressAlertTitle" = "Warning";
|
||||
"Wallet.Send.OwnAddressAlertText" = "Sending Grams from a wallet to the same wallet doesn't make sense, you will simply waste a portion of the value on blockchain fees.";
|
||||
"Wallet.Send.OwnAddressAlertProceed" = "Proceed";
|
||||
"Wallet.Send.TransactionInProgress" = "Please wait until the current transaction is completed.";
|
||||
"Wallet.Send.SyncInProgress" = "Please wait while the wallet finishes syncing with the TON Blockchain.";
|
||||
"Wallet.Send.EncryptComment" = "Encrypt Text";
|
||||
"Wallet.Settings.Title" = "Settings";
|
||||
"Wallet.Settings.Configuration" = "Server Settings";
|
||||
"Wallet.Settings.ConfigurationInfo" = "Advanced Settings";
|
||||
"Wallet.Settings.BackupWallet" = "Backup Wallet";
|
||||
"Wallet.Settings.DeleteWallet" = "Delete Wallet";
|
||||
"Wallet.Settings.DeleteWalletInfo" = "This will disconnect the wallet from this app. You will be able to restore your wallet using 24 secret words – or import another wallet.\n\nGram Wallets are located in the decentralized TON Blockchain. If you want a wallet to be deleted, simply transfer all the grams from it and leave it empty.";
|
||||
"Wallet.Intro.NotNow" = "Not Now";
|
||||
"Wallet.Intro.ImportExisting" = "Import existing wallet";
|
||||
"Wallet.Intro.CreateErrorTitle" = "An Error Occurred";
|
||||
"Wallet.Intro.CreateErrorText" = "Sorry. Please try again.";
|
||||
"Wallet.Intro.Title" = "Gram Wallet";
|
||||
"Wallet.Intro.Text" = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries.";
|
||||
"Wallet.Intro.CreateWallet" = "Create My Wallet";
|
||||
"Wallet.Intro.Terms" = "By creating a wallet you accept the\n[Terms of Conditions]().";
|
||||
"Wallet.Created.Title" = "Congratulations";
|
||||
"Wallet.Created.Text" = "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down your secret words and\nset up a secure passcode.";
|
||||
"Wallet.Created.Proceed" = "Proceed";
|
||||
"Wallet.Created.ExportErrorTitle" = "Error";
|
||||
"Wallet.Created.ExportErrorText" = "Encryption error. Please make sure you have enabled a device passcode in iOS settings and try again.";
|
||||
"Wallet.Completed.Title" = "Ready to go!";
|
||||
"Wallet.Completed.Text" = "You’re all set. Now you have a wallet that only you control - directly, without middlemen or bankers.";
|
||||
"Wallet.Completed.ViewWallet" = "View My Wallet";
|
||||
"Wallet.RestoreFailed.Title" = "Too Bad";
|
||||
"Wallet.RestoreFailed.Text" = "Without the secret words, you can't\nrestore access to your wallet.";
|
||||
"Wallet.RestoreFailed.CreateWallet" = "Create a New Wallet";
|
||||
"Wallet.RestoreFailed.EnterWords" = "Enter 24 words";
|
||||
"Wallet.Sending.Title" = "Sending Grams";
|
||||
"Wallet.Sending.Text" = "Please wait a few seconds for your transaction to be processed...";
|
||||
"Wallet.Sending.Title" = "Sending Grams";
|
||||
"Wallet.Sent.Title" = "Done!";
|
||||
"Wallet.Sent.Text" = "**%@ Grams** have been sent.";
|
||||
"Wallet.Sent.ViewWallet" = "View My Wallet";
|
||||
"Wallet.SecureStorageNotAvailable.Title" = "Set a Passcode";
|
||||
"Wallet.SecureStorageNotAvailable.Text" = "Please set up a Passcode on your device to enable secure payments with your Gram wallet.";
|
||||
"Wallet.SecureStorageReset.Title" = "Security Settings Have Changed";
|
||||
"Wallet.SecureStorageReset.BiometryTouchId" = "Touch ID";
|
||||
"Wallet.SecureStorageReset.BiometryFaceId" = "Face ID";
|
||||
"Wallet.SecureStorageReset.BiometryText" = "Unfortunately, your wallet is no longer available because your system Passcode or %@ has been turned off. Please enable them before proceeding.";
|
||||
"Wallet.SecureStorageReset.PasscodeText" = "Unfortunately, your wallet is no longer available because your system Passcode has been turned off. Please enable it before proceeding.";
|
||||
"Wallet.SecureStorageChanged.BiometryText" = "Unfortunately, your wallet is no longer available due to the change in your system security settings (Passcode/%@). To restore your wallet, tap \"Import existing wallet\".";
|
||||
"Wallet.SecureStorageChanged.PasscodeText" = "Unfortunately, your wallet is no longer available due to the change in your system security settings (Passcode). To restore your wallet, tap \"Import existing wallet\".";
|
||||
"Wallet.SecureStorageChanged.ImportWallet" = "Import Existing Wallet";
|
||||
"Wallet.SecureStorageChanged.CreateWallet" = "Create New Wallet";
|
||||
"Wallet.TransactionInfo.Title" = "Transaction";
|
||||
"Wallet.TransactionInfo.NoAddress" = "No Address";
|
||||
"Wallet.TransactionInfo.RecipientHeader" = "RECIPIENT";
|
||||
"Wallet.TransactionInfo.SenderHeader" = "SENDER";
|
||||
"Wallet.TransactionInfo.CopyAddress" = "Copy Wallet Address";
|
||||
"Wallet.TransactionInfo.AddressCopied" = "Address copied to clipboard.";
|
||||
"Wallet.TransactionInfo.SendGrams" = "Send Grams to This Address";
|
||||
"Wallet.TransactionInfo.CommentHeader" = "COMMENT";
|
||||
"Wallet.TransactionInfo.StorageFeeHeader" = "STORAGE FEE";
|
||||
"Wallet.TransactionInfo.OtherFeeHeader" = "TRANSACTION FEE";
|
||||
"Wallet.TransactionInfo.StorageFeeInfo" = "Blockchain validators collect a tiny fee for storing information about your decentralized wallet and processing your transactions.";
|
||||
"Wallet.TransactionInfo.StorageFeeInfoUrl" = "Blockchain validators collect a tiny fee for storing information about your decentralized wallet and processing your transactions. [More info]()";
|
||||
"Wallet.TransactionInfo.OtherFeeInfo" = "Blockchain validators collect a tiny fee for processing your decentralized transactions.";
|
||||
"Wallet.TransactionInfo.OtherFeeInfoUrl" = "Blockchain validators collect a tiny fee for processing your decentralized transactions. [More info]()";
|
||||
"Wallet.WordCheck.Title" = "Test Time!";
|
||||
"Wallet.WordCheck.Text" = "Let’s check that you wrote them down correctly. Please enter the words\n**%1$@**, **%2$@** and **%3$@**";
|
||||
"Wallet.WordCheck.Continue" = "Continue";
|
||||
"Wallet.WordCheck.IncorrectHeader" = "Incorrect words!";
|
||||
"Wallet.WordCheck.IncorrectText" = "The secret words you have entered do not match the ones in the list.";
|
||||
"Wallet.WordCheck.TryAgain" = "Try Again";
|
||||
"Wallet.WordCheck.ViewWords" = "View Words";
|
||||
"Wallet.WordImport.Title" = "24 Secret Words";
|
||||
"Wallet.WordImport.Text" = "Please restore access to your wallet by\nentering the 24 secret words you wrote down when creating the wallet.";
|
||||
"Wallet.WordImport.Continue" = "Continue";
|
||||
"Wallet.WordImport.CanNotRemember" = "I don't have them";
|
||||
"Wallet.WordImport.IncorrectTitle" = "Incorrect words";
|
||||
"Wallet.WordImport.IncorrectText" = "Sorry, you have entered incorrect secret words. Please double check and try again.";
|
||||
"Wallet.Words.Title" = "24 Secret Words";
|
||||
"Wallet.Words.Text" = "Write down these 24 words in the correct order and store them in a secret place.\n\nUse these secret words to restore access to your wallet if you lose your passcode or device.";
|
||||
"Wallet.Words.Done" = "Done";
|
||||
"Wallet.Words.NotDoneTitle" = "Sure Done?";
|
||||
"Wallet.Words.NotDoneText" = "You didn't have enough time to write those words down.";
|
||||
"Wallet.Words.NotDoneOk" = "OK, Sorry";
|
||||
"Wallet.Words.NotDoneResponse" = "Apologies Accepted";
|
||||
"Wallet.Send.NetworkErrorTitle" = "No network";
|
||||
"Wallet.Send.NetworkErrorText" = "Couldn't send grams. Please make sure your internet connection is working and try again.";
|
||||
"Wallet.Send.ErrorNotEnoughFundsTitle" = "Insufficient Grams";
|
||||
"Wallet.Send.ErrorNotEnoughFundsText" = "Unfortunately, your transfer couldn't be completed. You don't have enough grams.";
|
||||
"Wallet.Send.ErrorInvalidAddress" = "Invalid wallet address. Please correct and try again.";
|
||||
"Wallet.Send.ErrorDecryptionFailed" = "Please make sure that your device has a passcode set in iOS Settings and try again.";
|
||||
"Wallet.Send.UninitializedTitle" = "Warning";
|
||||
"Wallet.Send.UninitializedText" = "This address belongs to an empty wallet. Are you sure you want to transfer grams to it?";
|
||||
"Wallet.Send.SendAnyway" = "Send Anyway";
|
||||
"Wallet.Receive.CreateInvoice" = "Create Invoice";
|
||||
"Wallet.Receive.CreateInvoiceInfo" = "You can specify the amount and purpose of the payment to save the sender some time.";
|
||||
"Wallet.Configuration.Title" = "Server Settings";
|
||||
"Wallet.Configuration.Apply" = "Save";
|
||||
"Wallet.Configuration.SourceHeader" = "SOURCE";
|
||||
"Wallet.Configuration.SourceURL" = "URL";
|
||||
"Wallet.Configuration.SourceJSON" = "JSON";
|
||||
"Wallet.Configuration.SourceInfo" = "Using a different configuration allows you to change Lite Server addresses.";
|
||||
"Wallet.Configuration.BlockchainIdHeader" = "BLOCKCHAIN ID";
|
||||
"Wallet.Configuration.BlockchainIdPlaceholder" = "Blockchain ID";
|
||||
"Wallet.Configuration.BlockchainIdInfo" = "This setting is for developers. Change it only if you are working on creating your own TON network.";
|
||||
"Wallet.Configuration.ApplyErrorTitle" = "Error";
|
||||
"Wallet.Configuration.ApplyErrorTextURLInvalid" = "The URL you have entered is invalid. Please try again.";
|
||||
"Wallet.Configuration.ApplyErrorTextURLUnreachable" = "There was an error while downloading configuration from %@\nPlease try again.";
|
||||
"Wallet.Configuration.ApplyErrorTextURLInvalidData" = "This blockchain configuration is invalid. Please try again.";
|
||||
"Wallet.Configuration.ApplyErrorTextJSONInvalidData" = "This blockchain configuration is invalid. Please try again.";
|
||||
"Wallet.Configuration.BlockchainNameChangedTitle" = "Warning";
|
||||
"Wallet.Configuration.BlockchainNameChangedText" = "Are you sure you want to change the blockchain ID? You don't need this unless you're testing your own TON network.\n\nIf you proceed, you will need to reconnect your wallet using 24 secret words.";
|
||||
"Wallet.Configuration.BlockchainNameChangedProceed" = "Proceed";
|
||||
"Wallet.CreateInvoice.Title" = "Create Invoice";
|
||||
"Wallet.Navigation.Close" = "Close";
|
||||
"Wallet.Navigation.Back" = "Back";
|
||||
"Wallet.Navigation.Done" = "Done";
|
||||
"Wallet.Navigation.Cancel" = "Cancel";
|
||||
"Wallet.Alert.OK" = "OK";
|
||||
"Wallet.Alert.Cancel" = "Cancel";
|
||||
"Wallet.Month.GenJanuary" = "January";
|
||||
"Wallet.Month.GenFebruary" = "February";
|
||||
"Wallet.Month.GenMarch" = "March";
|
||||
"Wallet.Month.GenApril" = "April";
|
||||
"Wallet.Month.GenMay" = "May";
|
||||
"Wallet.Month.GenJune" = "June";
|
||||
"Wallet.Month.GenJuly" = "July";
|
||||
"Wallet.Month.GenAugust" = "August";
|
||||
"Wallet.Month.GenSeptember" = "September";
|
||||
"Wallet.Month.GenOctober" = "October";
|
||||
"Wallet.Month.GenNovember" = "November";
|
||||
"Wallet.Month.GenDecember" = "December";
|
||||
"Wallet.Month.ShortJanuary" = "Jan";
|
||||
"Wallet.Month.ShortFebruary" = "Feb";
|
||||
"Wallet.Month.ShortMarch" = "Mar";
|
||||
"Wallet.Month.ShortApril" = "Apr";
|
||||
"Wallet.Month.ShortMay" = "May";
|
||||
"Wallet.Month.ShortJune" = "Jun";
|
||||
"Wallet.Month.ShortJuly" = "Jul";
|
||||
"Wallet.Month.ShortAugust" = "Aug";
|
||||
"Wallet.Month.ShortSeptember" = "Sep";
|
||||
"Wallet.Month.ShortOctober" = "Oct";
|
||||
"Wallet.Month.ShortNovember" = "Nov";
|
||||
"Wallet.Month.ShortDecember" = "Dec";
|
||||
"Wallet.Weekday.Today" = "Today";
|
||||
"Wallet.Weekday.Yesterday" = "Yesterday";
|
||||
"Wallet.Info.TransactionDateHeader" = "%1$@ %2$@";
|
||||
"Wallet.Info.TransactionDateHeaderYear" = "%1$@ %2$@, %3$@";
|
||||
"Wallet.UnknownError" = "An error occurred. Please try again later.";
|
||||
"Wallet.ContextMenuCopy" = "Copy";
|
||||
"Wallet.Time.PreciseDate_m1" = "Jan %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m2" = "Feb %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m3" = "Mar %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m4" = "Apr %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m5" = "May %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m6" = "Jun %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m7" = "Jul %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m8" = "Aug %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m9" = "Sep %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m10" = "Oct %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m11" = "Nov %1$@, %2$@ at %3$@";
|
||||
"Wallet.Time.PreciseDate_m12" = "Dec %1$@, %2$@ at %3$@";
|
||||
"Wallet.VoiceOver.Editing.ClearText" = "Clear text";
|
||||
"Wallet.Receive.ShareInvoiceUrlInfo" = "Share this link with other Gram wallet owners to receive %@ Grams from them.";
|
||||
"Wallet.AccessDenied.Title" = "Please Allow Access";
|
||||
"Wallet.AccessDenied.Camera" = "TON Wallet needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set TON Wallet to ON.";
|
||||
"Wallet.AccessDenied.Settings" = "Settings";
|
@ -13,9 +13,12 @@ objc_library(
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
copts = [
|
||||
"-DTELEGRAM_USE_BORINGSSL=1",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/EncryptionProvider:EncryptionProvider",
|
||||
"//submodules/openssl:openssl",
|
||||
"//third-party/boringssl:crypto",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
|
@ -97,7 +97,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)setConstantTime:(id<MTBignum>)other {
|
||||
assert([other isKindOfClass:[MTBignumImpl class]]);
|
||||
MTBignumImpl *otherImpl = other;
|
||||
#ifndef TELEGRAM_USE_BORINGSSL
|
||||
BN_set_flags(otherImpl->_value, BN_FLG_CONSTTIME);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)assignWordTo:(id<MTBignum>)bignum value:(unsigned long)value {
|
||||
|
@ -13,8 +13,11 @@ objc_library(
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
copts = [
|
||||
"-DTELEGRAM_USE_BORINGSSL=1",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/openssl:openssl",
|
||||
"//third-party/boringssl:crypto",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
|
@ -38,6 +38,40 @@ static NSData * _Nullable readPublicKey(EVP_PKEY *subject) {
|
||||
|
||||
+ (MTPKCS * _Nullable)parse:(const unsigned char *)buffer size:(int)size {
|
||||
#if TARGET_OS_IOS
|
||||
#ifdef TELEGRAM_USE_BORINGSSL
|
||||
BIO *pkcsBio = BIO_new(BIO_s_mem());
|
||||
BIO_write(pkcsBio, buffer, size);
|
||||
STACK_OF(X509) *signers = NULL;
|
||||
PKCS7_get_PEM_certificates(signers, pkcsBio);
|
||||
if (signers == NULL) {
|
||||
BIO_free(pkcsBio);
|
||||
return nil;
|
||||
}
|
||||
|
||||
const X509* cert = sk_X509_pop(signers);
|
||||
if (cert == NULL) {
|
||||
if (signers) {
|
||||
sk_X509_free(signers);
|
||||
}
|
||||
BIO_free(pkcsBio);
|
||||
return nil;
|
||||
}
|
||||
|
||||
X509_NAME *issuerName = X509_get_issuer_name(cert);
|
||||
X509_NAME *subjectName = X509_get_subject_name(cert);
|
||||
|
||||
NSString *issuerNameString = readName(issuerName);
|
||||
NSString *subjectNameString = readName(subjectName);
|
||||
|
||||
EVP_PKEY *publicKey = X509_get_pubkey(cert);
|
||||
NSData *data = readPublicKey(publicKey);
|
||||
|
||||
MTPKCS *result = [[MTPKCS alloc] initWithIssuerName:issuerNameString subjectName:subjectNameString data:data];
|
||||
|
||||
BIO_free(pkcsBio);
|
||||
|
||||
return result;
|
||||
#else
|
||||
MTPKCS * _Nullable result = nil;
|
||||
PKCS7 *pkcs7 = NULL;
|
||||
STACK_OF(X509) *signers = NULL;
|
||||
@ -86,6 +120,7 @@ static NSData * _Nullable readPublicKey(EVP_PKEY *subject) {
|
||||
result = [[MTPKCS alloc] initWithIssuerName:issuerNameString subjectName:subjectNameString data:data];
|
||||
|
||||
return result;
|
||||
#endif
|
||||
#else
|
||||
return nil;
|
||||
#endif
|
||||
|
@ -2,34 +2,119 @@ import Foundation
|
||||
import SwiftSignalKit
|
||||
private typealias SignalKitTimer = SwiftSignalKit.Timer
|
||||
|
||||
struct InodeInfo {
|
||||
var inode: __darwin_ino64_t
|
||||
var timestamp: Int32
|
||||
var size: UInt32
|
||||
}
|
||||
|
||||
private func scanFiles(at path: String, olderThan minTimestamp: Int32, anyway: ((String, Int, Int32)) -> Void, unlink f: (String) -> Void) {
|
||||
guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey, .isDirectoryKey, .fileSizeKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else {
|
||||
private struct ScanFilesResult {
|
||||
var unlinkedCount = 0
|
||||
var totalSize: UInt64 = 0
|
||||
}
|
||||
|
||||
private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: inout [InodeInfo]) -> ScanFilesResult {
|
||||
var result = ScanFilesResult()
|
||||
|
||||
if let dp = opendir(path) {
|
||||
let pathBuffer = malloc(2048).assumingMemoryBound(to: Int8.self)
|
||||
defer {
|
||||
free(pathBuffer)
|
||||
}
|
||||
|
||||
while true {
|
||||
guard let dirp = readdir(dp) else {
|
||||
break
|
||||
}
|
||||
|
||||
if strncmp(&dirp.pointee.d_name.0, ".", 1024) == 0 {
|
||||
continue
|
||||
}
|
||||
if strncmp(&dirp.pointee.d_name.0, "..", 1024) == 0 {
|
||||
continue
|
||||
}
|
||||
strncpy(pathBuffer, path, 1024)
|
||||
strncat(pathBuffer, "/", 1024)
|
||||
strncat(pathBuffer, &dirp.pointee.d_name.0, 1024)
|
||||
|
||||
//puts(pathBuffer)
|
||||
//puts("\n")
|
||||
|
||||
var value = stat()
|
||||
if stat(pathBuffer, &value) == 0 {
|
||||
if value.st_mtimespec.tv_sec < minTimestamp {
|
||||
unlink(pathBuffer)
|
||||
result.unlinkedCount += 1
|
||||
} else {
|
||||
result.totalSize += UInt64(value.st_size)
|
||||
inodes.append(InodeInfo(
|
||||
inode: value.st_ino,
|
||||
timestamp: Int32(clamping: value.st_mtimespec.tv_sec),
|
||||
size: UInt32(clamping: value.st_size)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dp)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64) {
|
||||
var removedSize: UInt64 = 0
|
||||
|
||||
inodes.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp < rhs.timestamp
|
||||
})
|
||||
|
||||
var inodesToDelete = Set<__darwin_ino64_t>()
|
||||
|
||||
for inode in inodes {
|
||||
inodesToDelete.insert(inode.inode)
|
||||
removedSize += UInt64(inode.size)
|
||||
if removedSize >= removeSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if inodesToDelete.isEmpty {
|
||||
return
|
||||
}
|
||||
while let item = enumerator.nextObject() {
|
||||
guard let url = item as? NSURL else {
|
||||
continue
|
||||
}
|
||||
guard let resourceValues = try? url.resourceValues(forKeys: [.contentModificationDateKey, .isDirectoryKey, .fileSizeKey]) else {
|
||||
continue
|
||||
}
|
||||
if let value = resourceValues[.isDirectoryKey] as? Bool, value {
|
||||
continue
|
||||
}
|
||||
if let value = resourceValues[.contentModificationDateKey] as? NSDate {
|
||||
var unlinked = false
|
||||
if Int32(value.timeIntervalSince1970) < minTimestamp {
|
||||
if let file = url.path {
|
||||
f(file)
|
||||
unlinked = true
|
||||
}
|
||||
}
|
||||
if let file = url.path, !unlinked {
|
||||
if let size = (resourceValues[.fileSizeKey] as? NSNumber)?.intValue {
|
||||
anyway((file, size, Int32(value.timeIntervalSince1970)))
|
||||
|
||||
let pathBuffer = malloc(2048).assumingMemoryBound(to: Int8.self)
|
||||
defer {
|
||||
free(pathBuffer)
|
||||
}
|
||||
|
||||
for path in paths {
|
||||
if let dp = opendir(path) {
|
||||
while true {
|
||||
guard let dirp = readdir(dp) else {
|
||||
break
|
||||
}
|
||||
|
||||
if strncmp(&dirp.pointee.d_name.0, ".", 1024) == 0 {
|
||||
continue
|
||||
}
|
||||
if strncmp(&dirp.pointee.d_name.0, "..", 1024) == 0 {
|
||||
continue
|
||||
}
|
||||
strncpy(pathBuffer, path, 1024)
|
||||
strncat(pathBuffer, "/", 1024)
|
||||
strncat(pathBuffer, &dirp.pointee.d_name.0, 1024)
|
||||
|
||||
//puts(pathBuffer)
|
||||
//puts("\n")
|
||||
|
||||
var value = stat()
|
||||
if stat(pathBuffer, &value) == 0 {
|
||||
if inodesToDelete.contains(value.st_ino) {
|
||||
unlink(pathBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,49 +174,42 @@ private final class TimeBasedCleanupImpl {
|
||||
var removedShortLivedCount: Int = 0
|
||||
var removedGeneralCount: Int = 0
|
||||
var removedGeneralLimitCount: Int = 0
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
var inodes: [InodeInfo] = []
|
||||
var paths: [String] = []
|
||||
|
||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||
let bytesLimit = UInt64(gigabytesLimit) * 1024 * 1024 * 1024
|
||||
|
||||
let oldestShortLivedTimestamp = timestamp - shortLived
|
||||
let oldestGeneralTimestamp = timestamp - general
|
||||
for path in shortLivedPaths {
|
||||
scanFiles(at: path, olderThan: oldestShortLivedTimestamp, anyway: { _, _, _ in
|
||||
|
||||
}, unlink: { file in
|
||||
removedShortLivedCount += 1
|
||||
unlink(file)
|
||||
})
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestShortLivedTimestamp, inodes: &inodes)
|
||||
if !paths.contains(path) {
|
||||
paths.append(path)
|
||||
}
|
||||
removedShortLivedCount += scanResult.unlinkedCount
|
||||
}
|
||||
|
||||
var checkFiles: [GeneralFile] = []
|
||||
|
||||
var totalLimitSize: UInt64 = 0
|
||||
|
||||
for path in generalPaths {
|
||||
scanFiles(at: path, olderThan: oldestGeneralTimestamp, anyway: { file, size, timestamp in
|
||||
checkFiles.append(GeneralFile(file: file, size: size, timestamp: timestamp))
|
||||
totalLimitSize += UInt64(size)
|
||||
}, unlink: { file in
|
||||
removedGeneralCount += 1
|
||||
unlink(file)
|
||||
})
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestGeneralTimestamp, inodes: &inodes)
|
||||
if !paths.contains(path) {
|
||||
paths.append(path)
|
||||
}
|
||||
removedGeneralCount += scanResult.unlinkedCount
|
||||
totalLimitSize += scanResult.totalSize
|
||||
}
|
||||
|
||||
clear: for item in checkFiles.sorted(by: <) {
|
||||
if totalLimitSize > bytesLimit {
|
||||
unlink(item.file)
|
||||
removedGeneralLimitCount += 1
|
||||
if totalLimitSize > UInt64(item.size) {
|
||||
totalLimitSize -= UInt64(item.size)
|
||||
} else {
|
||||
totalLimitSize = 0
|
||||
}
|
||||
} else {
|
||||
break clear
|
||||
}
|
||||
if totalLimitSize > bytesLimit {
|
||||
mapFiles(paths: paths, inodes: &inodes, removeSize: totalLimitSize - bytesLimit)
|
||||
}
|
||||
|
||||
if removedShortLivedCount != 0 || removedGeneralCount != 0 || removedGeneralLimitCount != 0 {
|
||||
print("[TimeBasedCleanup] removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files")
|
||||
print("[TimeBasedCleanup] \(CFAbsoluteTimeGetCurrent() - startTime) s removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files")
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
@ -741,9 +741,15 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
guard let context = arguments.context else {
|
||||
return
|
||||
}
|
||||
let controller = GroupCallController(context: context)
|
||||
controller.navigationPresentation = .modal
|
||||
arguments.pushController(controller)
|
||||
let _ = (resolvePeerByName(account: context.account, name: "tgbetachat")
|
||||
|> deliverOnMainQueue).start(next: { peerId in
|
||||
guard let peerId = peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = VoiceChatController(context: context, peerId: peerId)
|
||||
arguments.presentController(controller, nil)
|
||||
})
|
||||
})
|
||||
case let .preferredVideoCodec(_, title, value, isSelected):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
|
@ -24,6 +24,9 @@ swift_library(
|
||||
"//submodules/TelegramCallsUI/CallsEmoji:CallsEmoji",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/ItemListPeerItem:ItemListPeerItem",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -14,6 +14,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
enum Color {
|
||||
case red
|
||||
case green
|
||||
case redDimmed
|
||||
case greenDimmed
|
||||
case grayDimmed
|
||||
}
|
||||
|
||||
case blurred(isFilled: Bool)
|
||||
@ -195,6 +198,12 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
fillColor = UIColor(rgb: 0xd92326)
|
||||
case .green:
|
||||
fillColor = UIColor(rgb: 0x74db58)
|
||||
case .redDimmed:
|
||||
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.3)
|
||||
case .greenDimmed:
|
||||
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3)
|
||||
case .grayDimmed:
|
||||
fillColor = UIColor(rgb: 0x1C1C1E)
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,6 +296,12 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
|
||||
case .green:
|
||||
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
|
||||
case .redDimmed:
|
||||
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
|
||||
case .greenDimmed:
|
||||
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
|
||||
case .grayDimmed:
|
||||
fillColor = UIColor(rgb: 0x1C1C1E).withAlphaComponent(0.2)
|
||||
}
|
||||
}
|
||||
|
||||
|
495
submodules/TelegramCallsUI/Sources/VoiceChatController.swift
Normal file
@ -0,0 +1,495 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramVoip
|
||||
import TelegramAudio
|
||||
import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import ItemListPeerItem
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import AppBundle
|
||||
import RadialStatusNode
|
||||
|
||||
private final class VoiceChatControllerTitleView: UIView {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let infoNode: ASTextNode
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.truncationMode = .byTruncatingTail
|
||||
self.titleNode.isOpaque = false
|
||||
|
||||
self.infoNode = ASTextNode()
|
||||
self.infoNode.displaysAsynchronously = false
|
||||
self.infoNode.maximumNumberOfLines = 1
|
||||
self.infoNode.truncationMode = .byTruncatingTail
|
||||
self.infoNode.isOpaque = false
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.infoNode)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func set(title: String, subtitle: String) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: .white)
|
||||
self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor.white.withAlphaComponent(0.5))
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if size.height > 40.0 {
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
let infoSize = self.infoNode.measure(size)
|
||||
let titleInfoSpacing: CGFloat = 0.0
|
||||
|
||||
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize)
|
||||
} else {
|
||||
let titleSize = self.titleNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height))
|
||||
let infoSize = self.infoNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height))
|
||||
|
||||
let titleInfoSpacing: CGFloat = 8.0
|
||||
let combinedWidth = titleSize.width + infoSize.width + titleInfoSpacing
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - infoSize.height) / 2.0)), size: infoSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let foregroundNode: ASImageNode
|
||||
|
||||
private var validSize: CGSize?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.foregroundNode = ASImageNode()
|
||||
|
||||
self.foregroundNode.image = UIImage(bundleImageName: "Call/VoiceChatMicOff")
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.foregroundNode)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
if self.validSize != size {
|
||||
self.validSize = size
|
||||
|
||||
self.backgroundNode.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1C1C1E))
|
||||
}
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let image = self.foregroundNode.image {
|
||||
self.foregroundNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class VoiceChatController: ViewController {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
private struct ListTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let isLoading: Bool
|
||||
let isEmpty: Bool
|
||||
let crossFade: Bool
|
||||
}
|
||||
|
||||
private final class Interaction {
|
||||
|
||||
}
|
||||
|
||||
private struct PeerEntry: Comparable, Identifiable {
|
||||
var participant: RenderedChannelParticipant
|
||||
var activityTimestamp: Int32
|
||||
|
||||
var stableId: PeerId {
|
||||
return self.participant.peer.id
|
||||
}
|
||||
|
||||
static func <(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
|
||||
if lhs.activityTimestamp != rhs.activityTimestamp {
|
||||
return lhs.activityTimestamp > rhs.activityTimestamp
|
||||
}
|
||||
return lhs.participant.peer.id < rhs.participant.peer.id
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListViewItem {
|
||||
let peer = self.participant.peer
|
||||
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: context, peer: peer, height: .peerList, presence: self.participant.presences[self.participant.peer.id], text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
|
||||
//arguments.deleteIncludePeer(peer.peerId)
|
||||
})]), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
|
||||
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
|
||||
}, removePeer: { id in
|
||||
//arguments.deleteIncludePeer(id)
|
||||
}, noInsets: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade)
|
||||
}
|
||||
|
||||
private weak var controller: VoiceChatController?
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private var presentationData: PresentationData
|
||||
private var darkTheme: PresentationTheme
|
||||
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
private let audioOutputNode: CallControllerButtonItemNode
|
||||
private let leaveNode: CallControllerButtonItemNode
|
||||
private let actionButton: VoiceChatActionButton
|
||||
private let radialStatus: RadialStatusNode
|
||||
private let statusLabel: ImmediateTextNode
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var didSetContentsReady: Bool = false
|
||||
private var didSetDataReady: Bool = false
|
||||
|
||||
private var currentEntries: [PeerEntry] = []
|
||||
private var peersDisposable: Disposable?
|
||||
|
||||
private var peerViewDisposable: Disposable?
|
||||
|
||||
private var itemInteraction: Interaction?
|
||||
|
||||
init(controller: VoiceChatController, context: AccountContext, peerId: PeerId) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.darkTheme = defaultDarkPresentationTheme
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.backgroundColor = self.darkTheme.list.itemBlocksBackgroundColor
|
||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
||||
self.listNode.clipsToBounds = true
|
||||
self.listNode.cornerRadius = 16.0
|
||||
|
||||
self.audioOutputNode = CallControllerButtonItemNode()
|
||||
self.leaveNode = CallControllerButtonItemNode()
|
||||
self.actionButton = VoiceChatActionButton()
|
||||
self.statusLabel = ImmediateTextNode()
|
||||
self.statusLabel.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(17.0), textColor: .white)
|
||||
|
||||
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
|
||||
super.init()
|
||||
|
||||
self.itemInteraction = Interaction()
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.contentContainer.addSubnode(self.listNode)
|
||||
self.contentContainer.addSubnode(self.audioOutputNode)
|
||||
self.contentContainer.addSubnode(self.leaveNode)
|
||||
self.contentContainer.addSubnode(self.actionButton)
|
||||
self.contentContainer.addSubnode(self.statusLabel)
|
||||
self.contentContainer.addSubnode(self.radialStatus)
|
||||
|
||||
self.addSubnode(self.contentContainer)
|
||||
|
||||
let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: self.context.account.peerId, peerId: self.peerId, updated: { [weak self] state in
|
||||
Queue.mainQueue().async {
|
||||
self?.updateMembers(members: state.list)
|
||||
}
|
||||
})
|
||||
|
||||
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case let .known(value) = offset, value < 40.0 {
|
||||
strongSelf.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: strongSelf.peerId, control: loadMoreControl)
|
||||
}
|
||||
}
|
||||
|
||||
self.peersDisposable = disposable
|
||||
|
||||
self.peerViewDisposable = (self.context.account.viewTracker.peerView(self.peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let peer = view.peers[view.peerId] else {
|
||||
return
|
||||
}
|
||||
var subtitle = "group"
|
||||
if let cachedData = view.cachedData as? CachedChannelData {
|
||||
if let memberCount = cachedData.participantsSummary.memberCount {
|
||||
subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(memberCount)
|
||||
}
|
||||
}
|
||||
|
||||
let titleView = VoiceChatControllerTitleView(theme: strongSelf.presentationData.theme)
|
||||
titleView.set(title: peer.debugDisplayTitle, subtitle: subtitle)
|
||||
strongSelf.controller?.navigationItem.titleView = titleView
|
||||
|
||||
if !strongSelf.didSetDataReady {
|
||||
strongSelf.didSetDataReady = true
|
||||
strongSelf.controller?.dataReady.set(true)
|
||||
}
|
||||
})
|
||||
|
||||
self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peersDisposable?.dispose()
|
||||
self.peerViewDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func leavePressed() {
|
||||
self.controller?.dismiss()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let bottomAreaHeight: CGFloat = 302.0
|
||||
|
||||
let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0)
|
||||
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y)))
|
||||
|
||||
transition.updateFrame(node: self.listNode, frame: listFrame)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listFrame.size, insets: UIEdgeInsets(top: -1.0, left: -6.0, bottom: -1.0, right: -6.0), scrollIndicatorInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0), duration: duration, curve: curve)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
let sideButtonSize = CGSize(width: 60.0, height: 60.0)
|
||||
let centralButtonSize = CGSize(width: 144.0, height: 144.0)
|
||||
let sideButtonInset: CGFloat = 27.0
|
||||
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.grayDimmed), image: .speaker), text: "audio", transition: .immediate)
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: "leave", transition: .immediate)
|
||||
|
||||
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonInset - sideButtonSize.width, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
|
||||
self.actionButton.updateLayout(size: centralButtonSize)
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
|
||||
let statusSize = self.statusLabel.updateLayout(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude))
|
||||
self.statusLabel.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: actionButtonFrame.maxY + 12.0), size: statusSize)
|
||||
|
||||
self.radialStatus.transitionToState(.progress(color: UIColor(rgb: 0x00ACFF), lineWidth: 3.3, value: nil, cancelEnabled: false), animated: false)
|
||||
self.radialStatus.frame = actionButtonFrame.insetBy(dx: -3.3, dy: -3.3)
|
||||
|
||||
if isFirstTime {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)?) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ListTransition) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
if let _ = self.validLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
|
||||
return
|
||||
}
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if transition.crossFade {
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.didSetContentsReady {
|
||||
strongSelf.didSetContentsReady = true
|
||||
strongSelf.controller?.contentsReady.set(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateMembers(members: [RenderedChannelParticipant]) {
|
||||
let previousEntries = self.currentEntries
|
||||
var entries: [PeerEntry] = []
|
||||
|
||||
var index: Int32 = 0
|
||||
for member in members {
|
||||
entries.append(PeerEntry(
|
||||
participant: member,
|
||||
activityTimestamp: Int32.max - 1 - index
|
||||
))
|
||||
index += 1
|
||||
}
|
||||
|
||||
self.currentEntries = entries
|
||||
|
||||
let presentationData = ItemListPresentationData(theme: self.darkTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings)
|
||||
|
||||
let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: context, presentationData: presentationData, interaction: self.itemInteraction!)
|
||||
self.enqueueTransition(transition)
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let presentationData: PresentationData
|
||||
|
||||
fileprivate let contentsReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
fileprivate let dataReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let _ready = Promise<Bool>(false)
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private var didAppearOnce: Bool = false
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: "Chat", target: self, action: #selector(self.closePressed))
|
||||
self.navigationItem.leftBarButtonItem = backItem
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
|
||||
self._ready.set(combineLatest([
|
||||
self.contentsReady.get(),
|
||||
self.dataReady.get()
|
||||
])
|
||||
|> map { values -> Bool in
|
||||
for value in values {
|
||||
if !value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|> filter { $0 })
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self, context: self.context, peerId: self.peerId)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.didAppearOnce {
|
||||
self.didAppearOnce = true
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Call/VoiceChatMicOff.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Voice.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/VoiceChatMicOff.imageset/Voice.pdf
vendored
Normal file
@ -48,7 +48,8 @@ objc_library(
|
||||
deps = [
|
||||
"//third-party/webrtc:webrtc_lib",
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/openssl:openssl",
|
||||
"//third-party/boringssl:crypto",
|
||||
"//third-party/boringssl:ssl",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
|
@ -17,7 +17,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SSignalKit:SSignalKit",
|
||||
"//submodules/openssl:openssl",
|
||||
"//third-party/boringssl:crypto",
|
||||
"//submodules/ton:ton",
|
||||
],
|
||||
visibility = [
|
||||
|
@ -14,7 +14,6 @@ swift_library(
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/WalletUrl:WalletUrl",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,15 +0,0 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "WalletCore",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/TonBinding:TonBinding",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
],
|
||||
)
|
@ -1,16 +0,0 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "WalletCore",
|
||||
module_name = "WalletCore",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/TonBinding:TonBinding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -1,52 +0,0 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
apple_resource(
|
||||
name = "WalletUIResources",
|
||||
files = glob([
|
||||
"Resources/**/*",
|
||||
], exclude = ["Resources/**/.*"]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_asset_catalog(
|
||||
name = 'WalletUIAssets',
|
||||
dirs = [
|
||||
"WalletImages.xcassets",
|
||||
],
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
static_library(
|
||||
name = "WalletUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/Camera:Camera",
|
||||
"//submodules/QrCode:QrCode",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/GlassButtonNode:GlassButtonNode",
|
||||
"//submodules/UrlEscaping:UrlEscaping",
|
||||
"//submodules/LocalAuth:LocalAuth",
|
||||
"//submodules/ScreenCaptureDetection:ScreenCaptureDetection",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/WalletUrl:WalletUrl",
|
||||
"//submodules/WalletCore:WalletCore",
|
||||
"//submodules/StringPluralization:StringPluralization",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
|
||||
"//submodules/Markdown:Markdown",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/CoreImage.framework",
|
||||
],
|
||||
)
|
@ -1,51 +0,0 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
filegroup(
|
||||
name = "WalletUIResources",
|
||||
srcs = glob([
|
||||
"Resources/**/*",
|
||||
], exclude = ["Resources/**/.*"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "WalletUIAssets",
|
||||
srcs = glob(["WalletImages.xcassets/**"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "WalletUI",
|
||||
module_name = "WalletUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
":WalletUIResources",
|
||||
":WalletUIAssets",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/Camera:Camera",
|
||||
"//submodules/QrCode:QrCode",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/GlassButtonNode:GlassButtonNode",
|
||||
"//submodules/UrlEscaping:UrlEscaping",
|
||||
"//submodules/LocalAuth:LocalAuth",
|
||||
"//submodules/ScreenCaptureDetection:ScreenCaptureDetection",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/StringPluralization:StringPluralization",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -1,580 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import ProgressNavigationButtonNode
|
||||
|
||||
enum ItemListNavigationButtonStyle {
|
||||
case regular
|
||||
case bold
|
||||
case activity
|
||||
|
||||
var barButtonItemStyle: UIBarButtonItem.Style {
|
||||
switch self {
|
||||
case .regular, .activity:
|
||||
return .plain
|
||||
case .bold:
|
||||
return .done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ItemListNavigationButtonContentIcon {
|
||||
case search
|
||||
case add
|
||||
case action
|
||||
}
|
||||
|
||||
enum ItemListNavigationButtonContent: Equatable {
|
||||
case none
|
||||
case text(String)
|
||||
case icon(ItemListNavigationButtonContentIcon)
|
||||
}
|
||||
|
||||
struct ItemListNavigationButton {
|
||||
let content: ItemListNavigationButtonContent
|
||||
let style: ItemListNavigationButtonStyle
|
||||
let enabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(content: ItemListNavigationButtonContent, style: ItemListNavigationButtonStyle, enabled: Bool, action: @escaping () -> Void) {
|
||||
self.content = content
|
||||
self.style = style
|
||||
self.enabled = enabled
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemListBackButton: Equatable {
|
||||
let title: String
|
||||
|
||||
init(title: String) {
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
enum ItemListControllerTitle: Equatable {
|
||||
case text(String)
|
||||
}
|
||||
|
||||
final class ItemListControllerTabBarItem: Equatable {
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
let selectedImage: UIImage?
|
||||
let tintImages: Bool
|
||||
let badgeValue: String?
|
||||
|
||||
init(title: String, image: UIImage?, selectedImage: UIImage?, tintImages: Bool = true, badgeValue: String? = nil) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.selectedImage = selectedImage
|
||||
self.tintImages = tintImages
|
||||
self.badgeValue = badgeValue
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemListControllerTabBarItem, rhs: ItemListControllerTabBarItem) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.image === rhs.image && lhs.selectedImage === rhs.selectedImage && lhs.tintImages == rhs.tintImages && lhs.badgeValue == rhs.badgeValue
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemListControllerState {
|
||||
let theme: WalletTheme
|
||||
let title: ItemListControllerTitle
|
||||
let leftNavigationButton: ItemListNavigationButton?
|
||||
let rightNavigationButton: ItemListNavigationButton?
|
||||
let secondaryRightNavigationButton: ItemListNavigationButton?
|
||||
let backNavigationButton: ItemListBackButton?
|
||||
let tabBarItem: ItemListControllerTabBarItem?
|
||||
let animateChanges: Bool
|
||||
|
||||
init(theme: WalletTheme, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, secondaryRightNavigationButton: ItemListNavigationButton? = nil, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.leftNavigationButton = leftNavigationButton
|
||||
self.rightNavigationButton = rightNavigationButton
|
||||
self.secondaryRightNavigationButton = secondaryRightNavigationButton
|
||||
self.backNavigationButton = backNavigationButton
|
||||
self.tabBarItem = tabBarItem
|
||||
self.animateChanges = animateChanges
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListController: ViewController, KeyShortcutResponder, PresentableController {
|
||||
private let state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>
|
||||
|
||||
private var leftNavigationButtonTitleAndStyle: (ItemListNavigationButtonContent, ItemListNavigationButtonStyle)?
|
||||
private var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle)] = []
|
||||
private var backNavigationButton: ItemListBackButton?
|
||||
private var tabBarItemInfo: ItemListControllerTabBarItem?
|
||||
private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil)
|
||||
|
||||
private var theme: WalletTheme
|
||||
private var strings: WalletStrings
|
||||
private var hasNavigationBarSeparator: Bool
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var didPlayPresentationAnimation = false
|
||||
private(set) var didAppearOnce = false
|
||||
var didAppear: ((Bool) -> Void)?
|
||||
private var isDismissed = false
|
||||
|
||||
var titleControlValueChanged: ((Int) -> Void)?
|
||||
|
||||
private var tabBarItemDisposable: Disposable?
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
var experimentalSnapScrollToItem: Bool = false {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var enableInteractiveDismiss = false {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).enableInteractiveDismiss = self.enableInteractiveDismiss
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var alwaysSynchronous = false {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).alwaysSynchronous = self.alwaysSynchronous
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).visibleEntriesUpdated = self.visibleEntriesUpdated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).contentOffsetChanged = self.contentOffsetChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contentScrollingEnded: ((ListView) -> Bool)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).contentScrollingEnded = self.contentScrollingEnded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchActivated: ((Bool) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).searchActivated = self.searchActivated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var willScrollToTop: (() -> Void)?
|
||||
|
||||
func setReorderEntry<T: ItemListNodeEntry>(_ f: @escaping (Int, Int, [T]) -> Void) {
|
||||
self.reorderEntry = { a, b, list in
|
||||
f(a, b, list.map { $0 as! T })
|
||||
}
|
||||
}
|
||||
private var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
(self.displayNode as! ItemListControllerNode).reorderEntry = self.reorderEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var previewItemWithTag: ((ItemListItemTag) -> UIViewController?)?
|
||||
var commitPreview: ((UIViewController) -> Void)?
|
||||
|
||||
var willDisappear: ((Bool) -> Void)?
|
||||
var didDisappear: ((Bool) -> Void)?
|
||||
|
||||
init<ItemGenerationArguments>(theme: WalletTheme, strings: WalletStrings, updatedPresentationData: Signal<(theme: WalletTheme, strings: WalletStrings), NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?, hasNavigationBarSeparator: Bool = true) {
|
||||
self.state = state
|
||||
|> map { controllerState, nodeStateAndArgument -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
return (controllerState, (nodeStateAndArgument.0, nodeStateAndArgument.1))
|
||||
}
|
||||
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.hasNavigationBarSeparator = hasNavigationBarSeparator
|
||||
|
||||
let navigationBarTheme: NavigationBarTheme
|
||||
if hasNavigationBarSeparator {
|
||||
navigationBarTheme = theme.navigationBar
|
||||
} else {
|
||||
navigationBarTheme = NavigationBarTheme(buttonColor: theme.navigationBar.buttonColor, disabledButtonColor: theme.navigationBar.disabledButtonColor, primaryTextColor: theme.navigationBar.primaryTextColor, backgroundColor: theme.list.itemBlocksBackgroundColor, separatorColor: theme.list.itemBlocksBackgroundColor, badgeBackgroundColor: theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.navigationBar.badgeStrokeColor, badgeTextColor: theme.navigationBar.badgeTextColor)
|
||||
}
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: strings.Wallet_Navigation_Back, close: strings.Wallet_Navigation_Close)))
|
||||
|
||||
self.isOpaqueWhenInOverlay = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.statusBar.statusBarStyle = theme.statusBarStyle
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
self?.willScrollToTop?()
|
||||
(self?.displayNode as! ItemListControllerNode).scrollToTop()
|
||||
}
|
||||
|
||||
if let tabBarItem = tabBarItem {
|
||||
self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] tabBarItemInfo in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.tabBarItemInfo != tabBarItemInfo {
|
||||
strongSelf.tabBarItemInfo = tabBarItemInfo
|
||||
|
||||
strongSelf.tabBarItem.title = tabBarItemInfo.title
|
||||
strongSelf.tabBarItem.image = tabBarItemInfo.image
|
||||
strongSelf.tabBarItem.selectedImage = tabBarItemInfo.selectedImage
|
||||
strongSelf.tabBarItem.badgeValue = tabBarItemInfo.badgeValue
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.tabBarItemDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
let previousControllerState = Atomic<ItemListControllerState?>(value: nil)
|
||||
let nodeState = self.state
|
||||
|> deliverOnMainQueue
|
||||
|> afterNext { [weak self] controllerState, state in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
let previousState = previousControllerState.swap(controllerState)
|
||||
if previousState?.title != controllerState.title {
|
||||
switch controllerState.title {
|
||||
case let .text(text):
|
||||
strongSelf.title = text
|
||||
strongSelf.navigationItem.titleView = nil
|
||||
}
|
||||
}
|
||||
strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action)
|
||||
|
||||
let themeUpdated = strongSelf.theme !== controllerState.theme
|
||||
if strongSelf.leftNavigationButtonTitleAndStyle?.0 != controllerState.leftNavigationButton?.content || strongSelf.leftNavigationButtonTitleAndStyle?.1 != controllerState.leftNavigationButton?.style || themeUpdated {
|
||||
if let leftNavigationButton = controllerState.leftNavigationButton {
|
||||
let item: UIBarButtonItem
|
||||
switch leftNavigationButton.content {
|
||||
case .none:
|
||||
item = UIBarButtonItem(title: "", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
|
||||
case let .text(value):
|
||||
item = UIBarButtonItem(title: value, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
|
||||
case let .icon(icon):
|
||||
var image: UIImage?
|
||||
switch icon {
|
||||
case .search:
|
||||
image = nil
|
||||
case .add:
|
||||
image = nil
|
||||
case .action:
|
||||
image = nil
|
||||
}
|
||||
item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
|
||||
}
|
||||
strongSelf.leftNavigationButtonTitleAndStyle = (leftNavigationButton.content, leftNavigationButton.style)
|
||||
strongSelf.navigationItem.setLeftBarButton(item, animated: false)
|
||||
item.isEnabled = leftNavigationButton.enabled
|
||||
} else {
|
||||
strongSelf.leftNavigationButtonTitleAndStyle = nil
|
||||
strongSelf.navigationItem.setLeftBarButton(nil, animated: false)
|
||||
}
|
||||
} else if let barButtonItem = strongSelf.navigationItem.leftBarButtonItem, let leftNavigationButton = controllerState.leftNavigationButton, leftNavigationButton.enabled != barButtonItem.isEnabled {
|
||||
barButtonItem.isEnabled = leftNavigationButton.enabled
|
||||
}
|
||||
|
||||
var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle, Bool)] = []
|
||||
if let secondaryRightNavigationButton = controllerState.secondaryRightNavigationButton {
|
||||
rightNavigationButtonTitleAndStyle.append((secondaryRightNavigationButton.content, secondaryRightNavigationButton.style, secondaryRightNavigationButton.enabled))
|
||||
}
|
||||
if let rightNavigationButton = controllerState.rightNavigationButton {
|
||||
rightNavigationButtonTitleAndStyle.append((rightNavigationButton.content, rightNavigationButton.style, rightNavigationButton.enabled))
|
||||
}
|
||||
|
||||
var updateRightButtonItems = false
|
||||
if rightNavigationButtonTitleAndStyle.count != strongSelf.rightNavigationButtonTitleAndStyle.count {
|
||||
updateRightButtonItems = true
|
||||
} else {
|
||||
for i in 0 ..< rightNavigationButtonTitleAndStyle.count {
|
||||
if rightNavigationButtonTitleAndStyle[i].0 != strongSelf.rightNavigationButtonTitleAndStyle[i].0 || rightNavigationButtonTitleAndStyle[i].1 != strongSelf.rightNavigationButtonTitleAndStyle[i].1 {
|
||||
updateRightButtonItems = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateRightButtonItems || themeUpdated {
|
||||
strongSelf.rightNavigationButtonTitleAndStyle = rightNavigationButtonTitleAndStyle.map { ($0.0, $0.1) }
|
||||
var items: [UIBarButtonItem] = []
|
||||
var index = 0
|
||||
for (content, style, _) in rightNavigationButtonTitleAndStyle {
|
||||
let item: UIBarButtonItem
|
||||
if case .activity = style {
|
||||
item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.theme.navigationBar.buttonColor))
|
||||
} else {
|
||||
let action: Selector = (index == 0 && rightNavigationButtonTitleAndStyle.count > 1) ? #selector(strongSelf.secondaryRightNavigationButtonPressed) : #selector(strongSelf.rightNavigationButtonPressed)
|
||||
switch content {
|
||||
case .none:
|
||||
item = UIBarButtonItem(title: "", style: style.barButtonItemStyle, target: strongSelf, action: action)
|
||||
case let .text(value):
|
||||
item = UIBarButtonItem(title: value, style: style.barButtonItemStyle, target: strongSelf, action: action)
|
||||
case let .icon(icon):
|
||||
var image: UIImage?
|
||||
switch icon {
|
||||
case .search:
|
||||
image = nil
|
||||
case .add:
|
||||
image = nil
|
||||
case .action:
|
||||
image = nil
|
||||
}
|
||||
item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action)
|
||||
}
|
||||
}
|
||||
items.append(item)
|
||||
index += 1
|
||||
}
|
||||
strongSelf.navigationItem.setRightBarButtonItems(items, animated: false)
|
||||
index = 0
|
||||
for (_, _, enabled) in rightNavigationButtonTitleAndStyle {
|
||||
items[index].isEnabled = enabled
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
for i in 0 ..< rightNavigationButtonTitleAndStyle.count {
|
||||
strongSelf.navigationItem.rightBarButtonItems?[i].isEnabled = rightNavigationButtonTitleAndStyle[i].2
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.backNavigationButton != controllerState.backNavigationButton {
|
||||
strongSelf.backNavigationButton = controllerState.backNavigationButton
|
||||
|
||||
if let backNavigationButton = strongSelf.backNavigationButton {
|
||||
strongSelf.navigationItem.backBarButtonItem = UIBarButtonItem(title: backNavigationButton.title, style: .plain, target: nil, action: nil)
|
||||
} else {
|
||||
strongSelf.navigationItem.backBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.theme !== controllerState.theme {
|
||||
strongSelf.theme = controllerState.theme
|
||||
|
||||
let navigationBarTheme: NavigationBarTheme
|
||||
if strongSelf.hasNavigationBarSeparator {
|
||||
navigationBarTheme = strongSelf.theme.navigationBar
|
||||
} else {
|
||||
navigationBarTheme = NavigationBarTheme(buttonColor: strongSelf.theme.navigationBar.buttonColor, disabledButtonColor: strongSelf.theme.navigationBar.disabledButtonColor, primaryTextColor: strongSelf.theme.navigationBar.primaryTextColor, backgroundColor: strongSelf.theme.list.itemBlocksBackgroundColor, separatorColor: strongSelf.theme.list.itemBlocksBackgroundColor, badgeBackgroundColor: strongSelf.theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: strongSelf.theme.navigationBar.badgeStrokeColor, badgeTextColor: strongSelf.theme.navigationBar.badgeTextColor)
|
||||
}
|
||||
|
||||
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: strongSelf.strings.Wallet_Navigation_Back, close: strongSelf.strings.Wallet_Navigation_Close)))
|
||||
strongSelf.statusBar.statusBarStyle = strongSelf.theme.statusBarStyle
|
||||
|
||||
var items = strongSelf.navigationItem.rightBarButtonItems ?? []
|
||||
for i in 0 ..< strongSelf.rightNavigationButtonTitleAndStyle.count {
|
||||
if case .activity = strongSelf.rightNavigationButtonTitleAndStyle[i].1 {
|
||||
items[i] = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.theme.navigationBar.buttonColor))!
|
||||
}
|
||||
}
|
||||
strongSelf.navigationItem.setRightBarButtonItems(items, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} |> map { ($0.theme, $1) }
|
||||
let displayNode = ItemListControllerNode(controller: self, navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in
|
||||
if let strongSelf = self {
|
||||
strongSelf.navigationOffset = offset
|
||||
}
|
||||
}, state: nodeState)
|
||||
displayNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss
|
||||
displayNode.alwaysSynchronous = self.alwaysSynchronous
|
||||
displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated
|
||||
displayNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged
|
||||
displayNode.contentOffsetChanged = self.contentOffsetChanged
|
||||
displayNode.contentScrollingEnded = self.contentScrollingEnded
|
||||
displayNode.searchActivated = self.searchActivated
|
||||
displayNode.reorderEntry = self.reorderEntry
|
||||
displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
|
||||
displayNode.requestLayout = { [weak self] transition in
|
||||
self?.requestLayout(transition: transition)
|
||||
}
|
||||
self.displayNode = displayNode
|
||||
super.displayNodeDidLoad()
|
||||
self._ready.set((self.displayNode as! ItemListControllerNode).ready)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
(self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition)
|
||||
}
|
||||
|
||||
@objc func leftNavigationButtonPressed() {
|
||||
self.navigationButtonActions.left?()
|
||||
}
|
||||
|
||||
@objc func rightNavigationButtonPressed() {
|
||||
self.navigationButtonActions.right?()
|
||||
}
|
||||
|
||||
@objc func secondaryRightNavigationButtonPressed() {
|
||||
self.navigationButtonActions.secondaryRight?()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.viewDidAppear(completion: {})
|
||||
}
|
||||
|
||||
func viewDidAppear(completion: @escaping () -> Void) {
|
||||
(self.displayNode as! ItemListControllerNode).listNode.preloadPages = true
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
|
||||
self.didPlayPresentationAnimation = true
|
||||
if case .modalSheet = presentationArguments.presentationAnimation {
|
||||
(self.displayNode as! ItemListControllerNode).animateIn(completion: {
|
||||
presentationArguments.completion?()
|
||||
completion()
|
||||
})
|
||||
self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring))
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
|
||||
let firstTime = !self.didAppearOnce
|
||||
self.didAppearOnce = true
|
||||
self.didAppear?(firstTime)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
self.willDisappear?(animated)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.didDisappear?(animated)
|
||||
}
|
||||
|
||||
func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? {
|
||||
var result: CGRect?
|
||||
(self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListViewItemNode {
|
||||
if predicate(itemNode) {
|
||||
result = itemNode.convert(itemNode.bounds, to: self.displayNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func forEachItemNode(_ f: (ListViewItemNode) -> Void) {
|
||||
(self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListViewItemNode {
|
||||
f(itemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true) {
|
||||
(self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated)
|
||||
}
|
||||
|
||||
func afterLayout(_ f: @escaping () -> Void) {
|
||||
(self.displayNode as! ItemListControllerNode).afterLayout(f)
|
||||
}
|
||||
|
||||
func previewingController(from sourceView: UIView, for location: CGPoint) -> (UIViewController, CGRect)? {
|
||||
guard let layout = self.validLayout, case .phone = layout.deviceMetrics.type else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let boundsSize = self.view.bounds.size
|
||||
let contentSize: CGSize
|
||||
if case .unknown = layout.deviceMetrics {
|
||||
contentSize = boundsSize
|
||||
} else {
|
||||
contentSize = layout.deviceMetrics.previewingContentSize(inLandscape: boundsSize.width > boundsSize.height)
|
||||
}
|
||||
|
||||
var selectedNode: ItemListItemNode?
|
||||
let listLocation = self.view.convert(location, to: (self.displayNode as! ItemListControllerNode).listNode.view)
|
||||
(self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in
|
||||
if itemNode.frame.contains(listLocation), let itemNode = itemNode as? ItemListItemNode {
|
||||
selectedNode = itemNode
|
||||
}
|
||||
}
|
||||
if let selectedNode = selectedNode as? (ItemListItemNode & ListViewItemNode), let tag = selectedNode.tag {
|
||||
var sourceRect = selectedNode.view.superview!.convert(selectedNode.frame, to: sourceView)
|
||||
sourceRect.size.height -= UIScreenPixel
|
||||
|
||||
if let controller = self.previewItemWithTag?(tag) {
|
||||
if let controller = controller as? ContainableController {
|
||||
controller.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate)
|
||||
}
|
||||
return (controller, sourceRect)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func clearItemNodesHighlight(animated: Bool = false) {
|
||||
(self.displayNode as! ItemListControllerNode).listNode.clearHighlightAnimated(animated)
|
||||
}
|
||||
|
||||
func previewingCommit(_ viewControllerToCommit: UIViewController) {
|
||||
self.commitPreview?(viewControllerToCommit)
|
||||
}
|
||||
|
||||
var keyShortcuts: [KeyShortcut] {
|
||||
return [KeyShortcut(input: UIKeyCommand.inputEscape, action: { [weak self] in
|
||||
if !(self?.navigationController?.topViewController is TabBarController) {
|
||||
_ = self?.navigationBar?.executeBack()
|
||||
}
|
||||
})]
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
protocol ItemListControllerEmptyStateItem {
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode
|
||||
}
|
||||
|
||||
class ItemListControllerEmptyStateItemNode: ASDisplayNode {
|
||||
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
}
|
@ -1,685 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import MergeLists
|
||||
|
||||
typealias ItemListSectionId = Int32
|
||||
|
||||
protocol ItemListNodeAnyEntry {
|
||||
var anyId: AnyHashable { get }
|
||||
var tag: ItemListItemTag? { get }
|
||||
func isLessThan(_ rhs: ItemListNodeAnyEntry) -> Bool
|
||||
func isEqual(_ rhs: ItemListNodeAnyEntry) -> Bool
|
||||
func item(_ arguments: Any) -> ListViewItem
|
||||
}
|
||||
|
||||
protocol ItemListNodeEntry: Comparable, Identifiable, ItemListNodeAnyEntry {
|
||||
var section: ItemListSectionId { get }
|
||||
}
|
||||
|
||||
extension ItemListNodeEntry {
|
||||
var anyId: AnyHashable {
|
||||
return self.stableId
|
||||
}
|
||||
|
||||
func isLessThan(_ rhs: ItemListNodeAnyEntry) -> Bool {
|
||||
return self < (rhs as! Self)
|
||||
}
|
||||
|
||||
func isEqual(_ rhs: ItemListNodeAnyEntry) -> Bool {
|
||||
return self == (rhs as! Self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemListNodeEntry {
|
||||
var tag: ItemListItemTag? { return nil }
|
||||
}
|
||||
|
||||
private struct ItemListNodeEntryTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func preparedItemListNodeEntryTransition(from fromEntries: [ItemListNodeAnyEntry], to toEntries: [ItemListNodeAnyEntry], arguments: Any) -> ItemListNodeEntryTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, isLess: { lhs, rhs in
|
||||
return lhs.isLessThan(rhs)
|
||||
}, isEqual: { lhs, rhs in
|
||||
return lhs.isEqual(rhs)
|
||||
}, getId: { value in
|
||||
return value.anyId
|
||||
})
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) }
|
||||
|
||||
return ItemListNodeEntryTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
enum ItemListStyle {
|
||||
case plain
|
||||
case blocks
|
||||
}
|
||||
|
||||
private struct ItemListNodeTransition {
|
||||
let theme: WalletTheme
|
||||
let entries: ItemListNodeEntryTransition
|
||||
let updateStyle: ItemListStyle?
|
||||
let emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
let searchItem: ItemListControllerSearch?
|
||||
let focusItemTag: ItemListItemTag?
|
||||
let ensureVisibleItemTag: ItemListItemTag?
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let firstTime: Bool
|
||||
let animated: Bool
|
||||
let animateAlpha: Bool
|
||||
let crossfade: Bool
|
||||
let mergedEntries: [ItemListNodeAnyEntry]
|
||||
let scrollEnabled: Bool
|
||||
}
|
||||
|
||||
final class ItemListNodeState {
|
||||
let entries: [ItemListNodeAnyEntry]
|
||||
let style: ItemListStyle
|
||||
let emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
let searchItem: ItemListControllerSearch?
|
||||
let animateChanges: Bool
|
||||
let crossfadeState: Bool
|
||||
let scrollEnabled: Bool
|
||||
let focusItemTag: ItemListItemTag?
|
||||
let ensureVisibleItemTag: ItemListItemTag?
|
||||
let initialScrollToItem: ListViewScrollToItem?
|
||||
|
||||
init<T: ItemListNodeEntry>(entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) {
|
||||
self.entries = entries.map { $0 }
|
||||
self.style = style
|
||||
self.emptyStateItem = emptyStateItem
|
||||
self.searchItem = searchItem
|
||||
self.crossfadeState = crossfadeState
|
||||
self.animateChanges = animateChanges
|
||||
self.focusItemTag = focusItemTag
|
||||
self.ensureVisibleItemTag = ensureVisibleItemTag
|
||||
self.initialScrollToItem = initialScrollToItem
|
||||
self.scrollEnabled = scrollEnabled
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemListNodeOpaqueState {
|
||||
let mergedEntries: [ItemListNodeAnyEntry]
|
||||
|
||||
init(mergedEntries: [ItemListNodeAnyEntry]) {
|
||||
self.mergedEntries = mergedEntries
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemListNodeVisibleEntries: Sequence {
|
||||
let iterate: () -> ItemListNodeAnyEntry?
|
||||
|
||||
init(iterate: @escaping () -> ItemListNodeAnyEntry?) {
|
||||
self.iterate = iterate
|
||||
}
|
||||
|
||||
func makeIterator() -> AnyIterator<ItemListNodeAnyEntry> {
|
||||
return AnyIterator { () -> ItemListNodeAnyEntry? in
|
||||
return self.iterate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemListControllerNodeView: UITracingLayerView, PreviewingHostView {
|
||||
var onLayout: (() -> Void)?
|
||||
|
||||
init(controller: ItemListController?) {
|
||||
self.controller = controller
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.onLayout?()
|
||||
}
|
||||
|
||||
private var inHitTest = false
|
||||
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.inHitTest {
|
||||
return super.hitTest(point, with: event)
|
||||
} else {
|
||||
self.inHitTest = true
|
||||
let result = self.hitTestImpl?(point, event)
|
||||
self.inHitTest = false
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
var previewingDelegate: PreviewingHostViewDelegate? {
|
||||
return PreviewingHostViewDelegate(controllerForLocation: { [weak self] sourceView, point in
|
||||
return self?.controller?.previewingController(from: sourceView, for: point)
|
||||
}, commitController: { [weak self] controller in
|
||||
self?.controller?.previewingCommit(controller)
|
||||
})
|
||||
}
|
||||
|
||||
weak var controller: ItemListController?
|
||||
}
|
||||
|
||||
class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var _ready = ValuePromise<Bool>()
|
||||
var ready: Signal<Bool, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
private let navigationBar: NavigationBar
|
||||
|
||||
let listNode: ListView
|
||||
private let leftOverlayNode: ASDisplayNode
|
||||
private let rightOverlayNode: ASDisplayNode
|
||||
private var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
private var emptyStateNode: ItemListControllerEmptyStateItemNode?
|
||||
|
||||
private var searchItem: ItemListControllerSearch?
|
||||
private var searchNode: ItemListControllerSearchNode?
|
||||
|
||||
private let transitionDisposable = MetaDisposable()
|
||||
|
||||
private var enqueuedTransitions: [ItemListNodeTransition] = []
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private var theme: WalletTheme?
|
||||
private var listStyle: ItemListStyle?
|
||||
|
||||
private var appliedFocusItemTag: ItemListItemTag?
|
||||
private var appliedEnsureVisibleItemTag: ItemListItemTag?
|
||||
|
||||
private var afterLayoutActions: [() -> Void] = []
|
||||
|
||||
let updateNavigationOffset: (CGFloat) -> Void
|
||||
var dismiss: (() -> Void)?
|
||||
|
||||
var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)?
|
||||
var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
|
||||
var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)?
|
||||
var contentScrollingEnded: ((ListView) -> Bool)?
|
||||
var searchActivated: ((Bool) -> Void)?
|
||||
var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Void)?
|
||||
var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
var enableInteractiveDismiss = false {
|
||||
didSet {
|
||||
}
|
||||
}
|
||||
|
||||
var alwaysSynchronous = false
|
||||
|
||||
init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(WalletTheme, (ItemListNodeState, Any)), NoError>) {
|
||||
self.navigationBar = navigationBar
|
||||
self.updateNavigationOffset = updateNavigationOffset
|
||||
|
||||
self.listNode = ListView()
|
||||
self.leftOverlayNode = ASDisplayNode()
|
||||
self.rightOverlayNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({ [weak controller] in
|
||||
return ItemListControllerNodeView(controller: controller)
|
||||
})
|
||||
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
||||
if let strongSelf = self, let visibleEntriesUpdated = strongSelf.visibleEntriesUpdated, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries {
|
||||
if let visible = displayedRange.visibleRange {
|
||||
let indexRange = (visible.firstIndex, visible.lastIndex)
|
||||
|
||||
var index = indexRange.0
|
||||
let iterator = ItemListNodeVisibleEntries(iterate: {
|
||||
var item: ItemListNodeAnyEntry?
|
||||
if index <= indexRange.1 {
|
||||
item = mergedEntries[index]
|
||||
}
|
||||
index += 1
|
||||
return item
|
||||
})
|
||||
visibleEntriesUpdated(iterator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.reorderItem = { [weak self] fromIndex, toIndex, opaqueTransactionState in
|
||||
if let strongSelf = self, let reorderEntry = strongSelf.reorderEntry, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries {
|
||||
if fromIndex >= 0 && fromIndex < mergedEntries.count && toIndex >= 0 && toIndex < mergedEntries.count {
|
||||
reorderEntry(fromIndex, toIndex, mergedEntries)
|
||||
}
|
||||
}
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
|
||||
self?.visibleBottomContentOffsetChanged?(offset)
|
||||
}
|
||||
|
||||
self.listNode.visibleContentOffsetChanged = { [weak self] offset in
|
||||
var inVoiceOver = false
|
||||
if let validLayout = self?.validLayout {
|
||||
inVoiceOver = validLayout.0.inVoiceOver
|
||||
}
|
||||
self?.contentOffsetChanged?(offset, inVoiceOver)
|
||||
}
|
||||
|
||||
self.listNode.didEndScrolling = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode)
|
||||
}
|
||||
}
|
||||
|
||||
let previousState = Atomic<ItemListNodeState?>(value: nil)
|
||||
self.transitionDisposable.set(((state |> map { theme, stateAndArguments -> ItemListNodeTransition in
|
||||
let (state, arguments) = stateAndArguments
|
||||
if state.entries.count > 1 {
|
||||
for i in 1 ..< state.entries.count {
|
||||
assert(state.entries[i - 1].isLessThan(state.entries[i]))
|
||||
}
|
||||
}
|
||||
let previous = previousState.swap(state)
|
||||
let transition = preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, arguments: arguments)
|
||||
var updatedStyle: ItemListStyle?
|
||||
if previous?.style != state.style {
|
||||
updatedStyle = state.style
|
||||
}
|
||||
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if previous == nil {
|
||||
scrollToItem = state.initialScrollToItem
|
||||
}
|
||||
|
||||
return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled)
|
||||
}) |> deliverOnMainQueue).start(next: { [weak self] transition in
|
||||
if let strongSelf = self {
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.transitionDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
(self.view as? ItemListControllerNodeView)?.onLayout = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.afterLayoutActions.isEmpty {
|
||||
let afterLayoutActions = strongSelf.afterLayoutActions
|
||||
strongSelf.afterLayoutActions = []
|
||||
for f in afterLayoutActions {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(self.view as? ItemListControllerNodeView)?.hitTestImpl = { [weak self] point, event in
|
||||
return self?.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss?()
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
var addedInsets: UIEdgeInsets?
|
||||
if layout.size.width > 480.0 {
|
||||
let inset = max(20.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
insets.left += inset
|
||||
insets.right += inset
|
||||
addedInsets = UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: inset)
|
||||
|
||||
if self.leftOverlayNode.supernode == nil {
|
||||
self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
|
||||
}
|
||||
if self.rightOverlayNode.supernode == nil {
|
||||
self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode)
|
||||
}
|
||||
} else {
|
||||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
|
||||
if self.leftOverlayNode.supernode != nil {
|
||||
self.leftOverlayNode.removeFromSupernode()
|
||||
}
|
||||
if self.rightOverlayNode.supernode != nil {
|
||||
self.rightOverlayNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
|
||||
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: insets.left, height: layout.size.height)
|
||||
self.rightOverlayNode.frame = CGRect(x: layout.size.width - insets.right, y: 0.0, width: insets.right, height: layout.size.height)
|
||||
|
||||
if let emptyStateNode = self.emptyStateNode {
|
||||
var layout = layout
|
||||
if let addedInsets = addedInsets {
|
||||
layout = layout.addedInsets(insets: addedInsets)
|
||||
}
|
||||
emptyStateNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
|
||||
if let searchNode = self.searchNode {
|
||||
searchNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
|
||||
let dequeue = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
if dequeue {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
|
||||
if !self.afterLayoutActions.isEmpty {
|
||||
let afterLayoutActions = self.afterLayoutActions
|
||||
self.afterLayoutActions = []
|
||||
for f in afterLayoutActions {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ItemListNodeTransition) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
if self.validLayout != nil {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransitions() {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
let transition = self.enqueuedTransitions.removeFirst()
|
||||
|
||||
if transition.theme !== self.theme {
|
||||
self.theme = transition.theme
|
||||
|
||||
if let listStyle = self.listStyle {
|
||||
switch listStyle {
|
||||
case .plain:
|
||||
self.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.listNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.leftOverlayNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.rightOverlayNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
case .blocks:
|
||||
self.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.listNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.leftOverlayNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.rightOverlayNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let updateStyle = transition.updateStyle {
|
||||
self.listStyle = updateStyle
|
||||
|
||||
if let _ = self.theme {
|
||||
switch updateStyle {
|
||||
case .plain:
|
||||
self.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.listNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.leftOverlayNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
self.rightOverlayNode.backgroundColor = transition.theme.list.plainBackgroundColor
|
||||
case .blocks:
|
||||
self.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.listNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.leftOverlayNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
self.rightOverlayNode.backgroundColor = transition.theme.list.blocksBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if transition.firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
} else if transition.animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
} else if transition.animateAlpha {
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
options.insert(.AnimateAlpha)
|
||||
} else if transition.crossfade {
|
||||
options.insert(.AnimateCrossfade)
|
||||
} else {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
}
|
||||
if self.alwaysSynchronous {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
}
|
||||
let focusItemTag = transition.focusItemTag
|
||||
let ensureVisibleItemTag = transition.ensureVisibleItemTag
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let item = transition.scrollToItem {
|
||||
scrollToItem = item
|
||||
} else if self.listNode.experimentalSnapScrollToItem, let ensureVisibleItemTag = ensureVisibleItemTag {
|
||||
for i in 0 ..< transition.mergedEntries.count {
|
||||
if let tag = transition.mergedEntries[i].tag, tag.isEqual(to: ensureVisibleItemTag) {
|
||||
scrollToItem = ListViewScrollToItem(index: i, position: ListViewScrollPosition.visible, animated: true, curve: .Default(duration: nil), directionHint: .Down)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateSearchItem = false
|
||||
if let searchItem = self.searchItem, let updatedSearchItem = transition.searchItem {
|
||||
updateSearchItem = !searchItem.isEqual(to: updatedSearchItem)
|
||||
} else if (self.searchItem != nil) != (transition.searchItem != nil) {
|
||||
updateSearchItem = true
|
||||
}
|
||||
if updateSearchItem {
|
||||
self.searchItem = transition.searchItem
|
||||
if let searchItem = transition.searchItem {
|
||||
let updatedTitleContentNode = searchItem.titleContentNode(current: self.navigationBar.contentNode as? (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode))
|
||||
if updatedTitleContentNode !== self.navigationBar.contentNode {
|
||||
if let titleContentNode = self.navigationBar.contentNode as? ItemListControllerSearchNavigationContentNode {
|
||||
titleContentNode.deactivate()
|
||||
}
|
||||
updatedTitleContentNode.setQueryUpdated { [weak self] query in
|
||||
if let strongSelf = self {
|
||||
strongSelf.searchNode?.queryUpdated(query)
|
||||
}
|
||||
}
|
||||
self.navigationBar.setContentNode(updatedTitleContentNode, animated: true)
|
||||
updatedTitleContentNode.activate()
|
||||
}
|
||||
|
||||
let updatedNode = searchItem.node(current: self.searchNode, titleContentNode: updatedTitleContentNode)
|
||||
if let searchNode = self.searchNode, updatedNode !== searchNode {
|
||||
searchNode.removeFromSupernode()
|
||||
}
|
||||
if self.searchNode !== updatedNode {
|
||||
self.searchNode = updatedNode
|
||||
if let validLayout = self.validLayout {
|
||||
updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
||||
}
|
||||
self.insertSubnode(updatedNode, belowSubnode: self.navigationBar)
|
||||
updatedNode.activate()
|
||||
}
|
||||
} else {
|
||||
if let searchNode = self.searchNode {
|
||||
self.searchNode = nil
|
||||
searchNode.deactivate()
|
||||
}
|
||||
|
||||
if let titleContentNode = self.navigationBar.contentNode {
|
||||
if let titleContentNode = titleContentNode as? ItemListControllerSearchNavigationContentNode {
|
||||
titleContentNode.deactivate()
|
||||
}
|
||||
self.navigationBar.setContentNode(nil, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: ItemListNodeOpaqueState(mergedEntries: transition.mergedEntries), completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
|
||||
var updatedFocusItemTag = false
|
||||
if let appliedFocusItemTag = strongSelf.appliedFocusItemTag, let focusItemTag = focusItemTag {
|
||||
updatedFocusItemTag = !appliedFocusItemTag.isEqual(to: focusItemTag)
|
||||
} else if (strongSelf.appliedFocusItemTag != nil) != (focusItemTag != nil) {
|
||||
updatedFocusItemTag = true
|
||||
}
|
||||
if updatedFocusItemTag {
|
||||
if let focusItemTag = focusItemTag {
|
||||
strongSelf.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ItemListItemNode {
|
||||
if let itemTag = itemNode.tag {
|
||||
if itemTag.isEqual(to: focusItemTag) {
|
||||
if let focusableNode = itemNode as? ItemListItemFocusableNode {
|
||||
focusableNode.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.appliedFocusItemTag = focusItemTag
|
||||
}
|
||||
}
|
||||
|
||||
var updatedEnsureVisibleItemTag = false
|
||||
if let appliedEnsureVisibleTag = strongSelf.appliedEnsureVisibleItemTag, let ensureVisibleItemTag = ensureVisibleItemTag {
|
||||
updatedEnsureVisibleItemTag = !appliedEnsureVisibleTag.isEqual(to: ensureVisibleItemTag)
|
||||
} else if (strongSelf.appliedEnsureVisibleItemTag != nil) != (ensureVisibleItemTag != nil) {
|
||||
updatedEnsureVisibleItemTag = true
|
||||
}
|
||||
if updatedEnsureVisibleItemTag {
|
||||
if let ensureVisibleItemTag = ensureVisibleItemTag {
|
||||
var applied = false
|
||||
strongSelf.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ItemListItemNode {
|
||||
if let itemTag = itemNode.tag {
|
||||
if itemTag.isEqual(to: ensureVisibleItemTag) {
|
||||
if let itemNode = itemNode as? ListViewItemNode {
|
||||
strongSelf.listNode.ensureItemNodeVisible(itemNode)
|
||||
applied = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if applied {
|
||||
strongSelf.appliedEnsureVisibleItemTag = ensureVisibleItemTag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
var updateEmptyStateItem = false
|
||||
if let emptyStateItem = self.emptyStateItem, let updatedEmptyStateItem = transition.emptyStateItem {
|
||||
updateEmptyStateItem = !emptyStateItem.isEqual(to: updatedEmptyStateItem)
|
||||
} else if (self.emptyStateItem != nil) != (transition.emptyStateItem != nil) {
|
||||
updateEmptyStateItem = true
|
||||
}
|
||||
if updateEmptyStateItem {
|
||||
self.emptyStateItem = transition.emptyStateItem
|
||||
if let emptyStateItem = transition.emptyStateItem {
|
||||
let updatedNode = emptyStateItem.node(current: self.emptyStateNode)
|
||||
if let emptyStateNode = self.emptyStateNode, updatedNode !== emptyStateNode {
|
||||
emptyStateNode.removeFromSupernode()
|
||||
}
|
||||
if self.emptyStateNode !== updatedNode {
|
||||
self.emptyStateNode = updatedNode
|
||||
if let validLayout = self.validLayout {
|
||||
updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
||||
}
|
||||
self.addSubnode(updatedNode)
|
||||
}
|
||||
} else if let emptyStateNode = self.emptyStateNode {
|
||||
emptyStateNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak emptyStateNode] _ in
|
||||
emptyStateNode?.removeFromSupernode()
|
||||
})
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.emptyStateNode = nil
|
||||
}
|
||||
}
|
||||
self.listNode.scrollEnabled = transition.scrollEnabled
|
||||
|
||||
if updateSearchItem {
|
||||
self.requestLayout?(.animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.searchNode?.scrollToTop()
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0
|
||||
self.updateNavigationOffset(-distanceFromEquilibrium)
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
targetContentOffset.pointee = scrollView.contentOffset
|
||||
|
||||
let scrollVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
|
||||
if abs(scrollVelocity.y) > 200.0 {
|
||||
self.animateOut()
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let searchNode = self.searchNode {
|
||||
if let result = searchNode.hitTest(point, with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func afterLayout(_ f: @escaping () -> Void) {
|
||||
self.afterLayoutActions.append(f)
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
protocol ItemListControllerSearchNavigationContentNode {
|
||||
func activate()
|
||||
func deactivate()
|
||||
|
||||
func setQueryUpdated(_ f: @escaping (String) -> Void)
|
||||
}
|
||||
|
||||
protocol ItemListControllerSearch {
|
||||
func isEqual(to: ItemListControllerSearch) -> Bool
|
||||
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode
|
||||
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode
|
||||
}
|
||||
|
||||
class ItemListControllerSearchNode: ASDisplayNode {
|
||||
func activate() {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
self?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
}
|
||||
|
||||
func queryUpdated(_ query: String) {
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
}
|
||||
|
@ -1,150 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
protocol ItemListItemTag {
|
||||
func isEqual(to other: ItemListItemTag) -> Bool
|
||||
}
|
||||
|
||||
protocol ItemListItem {
|
||||
var sectionId: ItemListSectionId { get }
|
||||
var tag: ItemListItemTag? { get }
|
||||
var isAlwaysPlain: Bool { get }
|
||||
var requestsNoInset: Bool { get }
|
||||
}
|
||||
|
||||
extension ItemListItem {
|
||||
//let accessoryItem: ListViewAccessoryItem?
|
||||
|
||||
var isAlwaysPlain: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var requestsNoInset: Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
protocol ItemListItemNode {
|
||||
var tag: ItemListItemTag? { get }
|
||||
}
|
||||
|
||||
protocol ItemListItemFocusableNode {
|
||||
func focus()
|
||||
}
|
||||
|
||||
enum ItemListInsetWithOtherSection {
|
||||
case none
|
||||
case full
|
||||
case reduced
|
||||
}
|
||||
|
||||
enum ItemListNeighbor {
|
||||
case none
|
||||
case otherSection(ItemListInsetWithOtherSection)
|
||||
case sameSection(alwaysPlain: Bool)
|
||||
}
|
||||
|
||||
struct ItemListNeighbors {
|
||||
var top: ItemListNeighbor
|
||||
var bottom: ItemListNeighbor
|
||||
|
||||
init(top: ItemListNeighbor, bottom: ItemListNeighbor) {
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
}
|
||||
}
|
||||
|
||||
func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottomItem: ItemListItem?) -> ItemListNeighbors {
|
||||
let topNeighbor: ItemListNeighbor
|
||||
if let topItem = topItem {
|
||||
if topItem.sectionId != item.sectionId {
|
||||
let topInset: ItemListInsetWithOtherSection
|
||||
if topItem.requestsNoInset {
|
||||
topInset = .none
|
||||
} else {
|
||||
if topItem is ItemListTextItem {
|
||||
topInset = .reduced
|
||||
} else {
|
||||
topInset = .full
|
||||
}
|
||||
}
|
||||
topNeighbor = .otherSection(topInset)
|
||||
} else {
|
||||
topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain)
|
||||
}
|
||||
} else {
|
||||
topNeighbor = .none
|
||||
}
|
||||
|
||||
let bottomNeighbor: ItemListNeighbor
|
||||
if let bottomItem = bottomItem {
|
||||
if bottomItem.sectionId != item.sectionId {
|
||||
let bottomInset: ItemListInsetWithOtherSection
|
||||
if bottomItem.requestsNoInset {
|
||||
bottomInset = .none
|
||||
} else {
|
||||
bottomInset = .full
|
||||
}
|
||||
bottomNeighbor = .otherSection(bottomInset)
|
||||
} else {
|
||||
bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain)
|
||||
}
|
||||
} else {
|
||||
bottomNeighbor = .none
|
||||
}
|
||||
|
||||
return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor)
|
||||
}
|
||||
|
||||
func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
var insets = UIEdgeInsets()
|
||||
switch neighbors.top {
|
||||
case .otherSection:
|
||||
insets.top += 22.0
|
||||
case .none, .sameSection:
|
||||
break
|
||||
}
|
||||
switch neighbors.bottom {
|
||||
case .none:
|
||||
insets.bottom += 22.0
|
||||
case .otherSection, .sameSection:
|
||||
break
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
let topInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .none:
|
||||
topInset = UIScreenPixel + 35.0
|
||||
case .sameSection:
|
||||
topInset = 0.0
|
||||
case let .otherSection(otherInset):
|
||||
switch otherInset {
|
||||
case .none:
|
||||
topInset = 0.0
|
||||
case .full:
|
||||
topInset = UIScreenPixel + 35.0
|
||||
case .reduced:
|
||||
topInset = UIScreenPixel + 16.0
|
||||
}
|
||||
}
|
||||
let bottomInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection, .otherSection:
|
||||
bottomInset = 0.0
|
||||
case .none:
|
||||
bottomInset = UIScreenPixel + 35.0
|
||||
}
|
||||
return UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0)
|
||||
}
|
||||
|
||||
func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool {
|
||||
return params.width > 480.0
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ActivityIndicator
|
||||
|
||||
final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyStateItem {
|
||||
let theme: WalletTheme
|
||||
|
||||
init(theme: WalletTheme) {
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
|
||||
return to is ItemListLoadingIndicatorEmptyStateItem
|
||||
}
|
||||
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
|
||||
if let current = current as? ItemListLoadingIndicatorEmptyStateItemNode {
|
||||
current.theme = self.theme
|
||||
return current
|
||||
} else {
|
||||
return ItemListLoadingIndicatorEmptyStateItemNode(theme: self.theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
||||
var theme: WalletTheme {
|
||||
didSet {
|
||||
self.indicator.type = .custom(self.theme.list.itemAccentColor, 40.0, 2.0, false)
|
||||
}
|
||||
}
|
||||
private let indicator: ActivityIndicator
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
init(theme: WalletTheme) {
|
||||
self.theme = theme
|
||||
self.indicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 22.0, 2.0, false))
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.indicator)
|
||||
}
|
||||
|
||||
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
let size = CGSize(width: 22.0, height: 22.0)
|
||||
transition.updateFrame(node: self.indicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size))
|
||||
}
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListActionKind {
|
||||
case generic
|
||||
case destructive
|
||||
case neutral
|
||||
case disabled
|
||||
}
|
||||
|
||||
enum ItemListActionAlignment {
|
||||
case natural
|
||||
case center
|
||||
}
|
||||
|
||||
class ItemListActionItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let title: String
|
||||
let kind: ItemListActionKind
|
||||
let alignment: ItemListActionAlignment
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let action: () -> Void
|
||||
let longTapAction: (() -> Void)?
|
||||
let clearHighlightAutomatically: Bool
|
||||
let tag: Any?
|
||||
|
||||
init(theme: WalletTheme, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: (() -> Void)? = nil, clearHighlightAutomatically: Bool = true, tag: Any? = nil) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.kind = kind
|
||||
self.alignment = alignment
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
self.clearHighlightAutomatically = clearHighlightAutomatically
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListActionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
func selected(listView: ListView){
|
||||
if self.clearHighlightAutomatically {
|
||||
listView.clearHighlightAnimated(true)
|
||||
}
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let titleNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListActionItem?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag as? ItemListItemTag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
self.maskNode = ASImageNode()
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let textColor: UIColor
|
||||
switch item.kind {
|
||||
case .destructive:
|
||||
textColor = item.theme.list.itemDestructiveColor
|
||||
case .generic:
|
||||
textColor = item.theme.list.itemAccentColor
|
||||
case .neutral:
|
||||
textColor = item.theme.list.itemPrimaryTextColor
|
||||
case .disabled:
|
||||
textColor = item.theme.list.itemDisabledTextColor
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 44.0)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 44.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
|
||||
var accessibilityTraits: UIAccessibilityTraits = .button
|
||||
switch item.kind {
|
||||
case .disabled:
|
||||
accessibilityTraits.insert(.notEnabled)
|
||||
default:
|
||||
break
|
||||
}
|
||||
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 16.0 + params.leftInset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
switch item.alignment {
|
||||
case .natural:
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
case .center:
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && self.item?.kind != ItemListActionKind.disabled {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func longTapped() {
|
||||
self.item?.longTapAction?()
|
||||
}
|
||||
|
||||
override var canBeLongTapped: Bool {
|
||||
return self.item?.longTapAction != nil
|
||||
}
|
||||
}
|
@ -1,316 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public func generateItemListCheckIcon(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(1.98)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
context.translateBy(x: 1.0, y: 1.0)
|
||||
|
||||
let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ")
|
||||
})
|
||||
}
|
||||
|
||||
public enum ItemListCheckboxItemStyle {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
public enum ItemListCheckboxItemColor {
|
||||
case accent
|
||||
}
|
||||
|
||||
class ItemListCheckboxItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let title: String
|
||||
let style: ItemListCheckboxItemStyle
|
||||
let color: ItemListCheckboxItemColor
|
||||
let checked: Bool
|
||||
let zeroSeparatorInsets: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
|
||||
init(theme: WalletTheme, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.style = style
|
||||
self.color = color
|
||||
self.checked = checked
|
||||
self.zeroSeparatorInsets = zeroSeparatorInsets
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListCheckboxItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListCheckboxItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var selectable: Bool = true
|
||||
|
||||
public func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
public class ItemListCheckboxItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
|
||||
private var item: ItemListCheckboxItem?
|
||||
|
||||
public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
self.activateArea.activate = { [weak self] in
|
||||
self?.item?.action()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var leftInset: CGFloat = params.leftInset
|
||||
|
||||
switch item.style {
|
||||
case .left:
|
||||
leftInset += 44.0
|
||||
case .right:
|
||||
leftInset += 16.0
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let contentSize = CGSize(width: params.width, height: 44.0)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
var updateCheckImage: UIImage?
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
if currentItem?.theme !== item.theme || currentItem?.color != item.color {
|
||||
switch item.color {
|
||||
case .accent:
|
||||
updateCheckImage = generateItemListCheckIcon(color: item.theme.list.itemAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
if item.checked {
|
||||
strongSelf.activateArea.accessibilityValue = "Selected"
|
||||
} else {
|
||||
strongSelf.activateArea.accessibilityValue = ""
|
||||
}
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
|
||||
if let updateCheckImage = updateCheckImage {
|
||||
strongSelf.iconNode.image = updateCheckImage
|
||||
}
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
if let image = strongSelf.iconNode.image {
|
||||
switch item.style {
|
||||
case .left:
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
case .right:
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
}
|
||||
strongSelf.iconNode.isHidden = !item.checked
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
if item.zeroSeparatorInsets {
|
||||
bottomStripeInset = 0.0
|
||||
} else {
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -1,516 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListDisclosureItemTitleColor {
|
||||
case primary
|
||||
case accent
|
||||
}
|
||||
|
||||
enum ItemListDisclosureStyle {
|
||||
case arrow
|
||||
case none
|
||||
}
|
||||
|
||||
enum ItemListDisclosureLabelStyle {
|
||||
case text
|
||||
case detailText
|
||||
case multilineDetailText
|
||||
case badge(UIColor)
|
||||
case color(UIColor)
|
||||
}
|
||||
|
||||
class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let icon: UIImage?
|
||||
let title: String
|
||||
let titleColor: ItemListDisclosureItemTitleColor
|
||||
let enabled: Bool
|
||||
let label: String
|
||||
let labelStyle: ItemListDisclosureLabelStyle
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let disclosureStyle: ItemListDisclosureStyle
|
||||
let action: (() -> Void)?
|
||||
let clearHighlightAutomatically: Bool
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: WalletTheme, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil) {
|
||||
self.theme = theme
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.titleColor = titleColor
|
||||
self.enabled = enabled
|
||||
self.labelStyle = labelStyle
|
||||
self.label = label
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.disclosureStyle = disclosureStyle
|
||||
self.action = action
|
||||
self.clearHighlightAutomatically = clearHighlightAutomatically
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListDisclosureItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListDisclosureItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
func selected(listView: ListView){
|
||||
if self.clearHighlightAutomatically {
|
||||
listView.clearHighlightAnimated(true)
|
||||
}
|
||||
if self.enabled {
|
||||
self.action?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
private let badgeFont = Font.regular(15.0)
|
||||
private let detailFont = Font.regular(13.0)
|
||||
|
||||
class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
let iconNode: ASImageNode
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
let arrowNode: ASImageNode
|
||||
let labelBadgeNode: ASImageNode
|
||||
let labelImageNode: ASImageNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListDisclosureItem?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
if let item = self.item, let _ = item.action {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.displayWithoutProcessing = true
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
self.arrowNode.isLayerBacked = true
|
||||
|
||||
self.labelBadgeNode = ASImageNode()
|
||||
self.labelImageNode = ASImageNode()
|
||||
self.labelBadgeNode.displayWithoutProcessing = true
|
||||
self.labelBadgeNode.displaysAsynchronously = false
|
||||
self.labelBadgeNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.arrowNode)
|
||||
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
let currentHasBadge = self.labelBadgeNode.image != nil
|
||||
|
||||
return { item, params, neighbors in
|
||||
let rightInset: CGFloat
|
||||
switch item.disclosureStyle {
|
||||
case .none:
|
||||
rightInset = 16.0 + params.rightInset
|
||||
case .arrow:
|
||||
rightInset = 34.0 + params.rightInset
|
||||
}
|
||||
|
||||
var updateArrowImage: UIImage?
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
var updatedLabelBadgeImage: UIImage?
|
||||
var updatedLabelImage: UIImage?
|
||||
|
||||
var badgeColor: UIColor?
|
||||
if case let .badge(color) = item.labelStyle {
|
||||
if item.label.count > 0 {
|
||||
badgeColor = color
|
||||
}
|
||||
}
|
||||
if case let .color(color) = item.labelStyle {
|
||||
var updatedColor = true
|
||||
if let currentItem = currentItem, case let .color(previousColor) = currentItem.labelStyle, color.isEqual(previousColor) {
|
||||
updatedColor = false
|
||||
}
|
||||
if updatedColor {
|
||||
updatedLabelImage = generateFilledCircleImage(diameter: 17.0, color: color)
|
||||
}
|
||||
}
|
||||
|
||||
let badgeDiameter: CGFloat = 20.0
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
updateArrowImage = disclosureArrowImage(item.theme)
|
||||
if let badgeColor = badgeColor {
|
||||
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
|
||||
}
|
||||
} else if let badgeColor = badgeColor, !currentHasBadge {
|
||||
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
|
||||
}
|
||||
|
||||
var updateIcon = false
|
||||
if currentItem?.icon != item.icon {
|
||||
updateIcon = true
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
var leftInset = 16.0 + params.leftInset
|
||||
if let _ = item.icon {
|
||||
leftInset += 43.0
|
||||
}
|
||||
|
||||
let titleColor: UIColor
|
||||
if item.enabled {
|
||||
titleColor = item.titleColor == .accent ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor
|
||||
} else {
|
||||
titleColor = item.theme.list.itemDisabledTextColor
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let labelFont: UIFont
|
||||
let labelBadgeColor: UIColor
|
||||
var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
|
||||
switch item.labelStyle {
|
||||
case .badge:
|
||||
labelBadgeColor = item.theme.list.plainBackgroundColor
|
||||
labelFont = badgeFont
|
||||
case .detailText, .multilineDetailText:
|
||||
labelBadgeColor = item.theme.list.itemSecondaryTextColor
|
||||
labelFont = detailFont
|
||||
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
|
||||
default:
|
||||
labelBadgeColor = item.theme.list.itemSecondaryTextColor
|
||||
labelFont = titleFont
|
||||
}
|
||||
var multilineLabel = false
|
||||
if case .multilineDetailText = item.labelStyle {
|
||||
multilineLabel = true
|
||||
}
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let height: CGFloat
|
||||
switch item.labelStyle {
|
||||
case .detailText:
|
||||
height = 64.0
|
||||
case .multilineDetailText:
|
||||
height = 44.0 + labelLayout.size.height
|
||||
default:
|
||||
height = 44.0
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
strongSelf.activateArea.accessibilityValue = item.label
|
||||
if item.enabled {
|
||||
strongSelf.activateArea.accessibilityTraits = []
|
||||
} else {
|
||||
strongSelf.activateArea.accessibilityTraits = .notEnabled
|
||||
}
|
||||
|
||||
if let icon = item.icon {
|
||||
if strongSelf.iconNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.iconNode)
|
||||
}
|
||||
if updateIcon {
|
||||
strongSelf.iconNode.image = icon
|
||||
}
|
||||
let iconY: CGFloat
|
||||
if case .multilineDetailText = item.labelStyle {
|
||||
iconY = 14.0
|
||||
} else {
|
||||
iconY = floor((layout.contentSize.height - icon.size.height) / 2.0)
|
||||
}
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size)
|
||||
} else if strongSelf.iconNode.supernode != nil {
|
||||
strongSelf.iconNode.image = nil
|
||||
strongSelf.iconNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let updateArrowImage = updateArrowImage {
|
||||
strongSelf.arrowNode.image = updateArrowImage
|
||||
}
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = labelApply()
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
|
||||
if let updateBadgeImage = updatedLabelBadgeImage {
|
||||
if strongSelf.labelBadgeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode)
|
||||
}
|
||||
strongSelf.labelBadgeNode.image = updateBadgeImage
|
||||
}
|
||||
if badgeColor == nil && strongSelf.labelBadgeNode.supernode != nil {
|
||||
strongSelf.labelBadgeNode.image = nil
|
||||
strongSelf.labelBadgeNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0)
|
||||
strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: 12.0), size: CGSize(width: badgeWidth, height: badgeDiameter))
|
||||
|
||||
let labelFrame: CGRect
|
||||
switch item.labelStyle {
|
||||
case .badge:
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: 13.0), size: labelLayout.size)
|
||||
case .detailText, .multilineDetailText:
|
||||
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: 36.0), size: labelLayout.size)
|
||||
default:
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size)
|
||||
}
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
if case .color = item.labelStyle {
|
||||
if let updatedLabelImage = updatedLabelImage {
|
||||
strongSelf.labelImageNode.image = updatedLabelImage
|
||||
}
|
||||
if strongSelf.labelImageNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.labelImageNode)
|
||||
}
|
||||
if let image = strongSelf.labelImageNode.image {
|
||||
strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 50.0, y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
} else if strongSelf.labelImageNode.supernode != nil {
|
||||
strongSelf.labelImageNode.removeFromSupernode()
|
||||
strongSelf.labelImageNode.image = nil
|
||||
}
|
||||
|
||||
if let arrowImage = strongSelf.arrowNode.image {
|
||||
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
|
||||
}
|
||||
|
||||
switch item.disclosureStyle {
|
||||
case .none:
|
||||
strongSelf.arrowNode.isHidden = true
|
||||
case .arrow:
|
||||
strongSelf.arrowNode.isHidden = false
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && (self.item?.enabled ?? false) {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -1,470 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListMultilineInputItemTextLimitMode {
|
||||
case characters
|
||||
case bytes
|
||||
}
|
||||
|
||||
struct ItemListMultilineInputItemTextLimit {
|
||||
let value: Int
|
||||
let display: Bool
|
||||
let mode: ItemListMultilineInputItemTextLimitMode
|
||||
|
||||
init(value: Int, display: Bool, mode: ItemListMultilineInputItemTextLimitMode = .characters) {
|
||||
self.value = value
|
||||
self.display = display
|
||||
self.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemListMultilineInputInlineAction {
|
||||
let icon: UIImage
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(icon: UIImage, action: (() -> Void)?) {
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListMultilineInputItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let text: String
|
||||
let placeholder: String
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let capitalization: Bool
|
||||
let autocorrection: Bool
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let action: (() -> Void)?
|
||||
let textUpdated: (String) -> Void
|
||||
let shouldUpdateText: (String) -> Bool
|
||||
let processPaste: ((String) -> Void)?
|
||||
let updatedFocus: ((Bool) -> Void)?
|
||||
let maxLength: ItemListMultilineInputItemTextLimit?
|
||||
let minimalHeight: CGFloat?
|
||||
let inlineAction: ItemListMultilineInputInlineAction?
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: WalletTheme, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.placeholder = placeholder
|
||||
self.maxLength = maxLength
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.capitalization = capitalization
|
||||
self.autocorrection = autocorrection
|
||||
self.returnKeyType = returnKeyType
|
||||
self.minimalHeight = minimalHeight
|
||||
self.textUpdated = textUpdated
|
||||
self.shouldUpdateText = shouldUpdateText
|
||||
self.processPaste = processPaste
|
||||
self.updatedFocus = updatedFocus
|
||||
self.tag = tag
|
||||
self.action = action
|
||||
self.inlineAction = inlineAction
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListMultilineInputItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListMultilineInputItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private let textNode: EditableTextNode
|
||||
private let measureTextNode: TextNode
|
||||
|
||||
private let limitTextNode: TextNode
|
||||
private var inlineActionButtonNode: HighlightableButtonNode?
|
||||
|
||||
private var item: ItemListMultilineInputItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.textClippingNode = ASDisplayNode()
|
||||
self.textClippingNode.clipsToBounds = true
|
||||
|
||||
self.textNode = EditableTextNode()
|
||||
self.measureTextNode = TextNode()
|
||||
|
||||
self.limitTextNode = TextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.textClippingNode.addSubnode(self.textNode)
|
||||
self.addSubnode(self.textClippingNode)
|
||||
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
var textColor: UIColor = .black
|
||||
if let item = self.item {
|
||||
textColor = item.theme.list.itemPrimaryTextColor
|
||||
}
|
||||
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.delegate = self
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListMultilineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
|
||||
let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: WalletTheme?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.rightInset
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
}
|
||||
|
||||
var limitTextString: NSAttributedString?
|
||||
var rightInset: CGFloat = params.rightInset
|
||||
|
||||
if let maxLength = item.maxLength, maxLength.display {
|
||||
let textLength: Int
|
||||
switch maxLength.mode {
|
||||
case .characters:
|
||||
textLength = item.text.count
|
||||
case .bytes:
|
||||
textLength = item.text.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
|
||||
}
|
||||
let displayTextLimit = textLength > maxLength.value * 70 / 100
|
||||
let remainingCount = maxLength.value - textLength
|
||||
if displayTextLimit {
|
||||
limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
|
||||
rightInset += 30.0 + 4.0
|
||||
}
|
||||
|
||||
let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
if limitTextLayout.size.width > 30.0 {
|
||||
rightInset += 30.0
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
rightInset += inlineAction.icon.size.width + 8.0
|
||||
}
|
||||
|
||||
var measureText = item.text
|
||||
if measureText.hasSuffix("\n") || measureText.isEmpty {
|
||||
measureText += "|"
|
||||
}
|
||||
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
|
||||
let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
var contentHeight: CGFloat = textLayout.size.height + textTopInset + textBottomInset
|
||||
if let minimalHeight = item.minimalHeight {
|
||||
contentHeight = max(minimalHeight, contentHeight)
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: contentHeight)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor]
|
||||
strongSelf.textNode.tintColor = item.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.theme.list.itemAccentColor), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
let capitalizationType: UITextAutocapitalizationType = item.capitalization ? .sentences : .none
|
||||
let autocorrectionType: UITextAutocorrectionType = item.autocorrection ? .default : .no
|
||||
|
||||
if strongSelf.textNode.textView.autocapitalizationType != capitalizationType {
|
||||
strongSelf.textNode.textView.autocapitalizationType = capitalizationType
|
||||
}
|
||||
if strongSelf.textNode.textView.autocorrectionType != autocorrectionType {
|
||||
strongSelf.textNode.textView.autocorrectionType = autocorrectionType
|
||||
}
|
||||
if strongSelf.textNode.textView.returnKeyType != item.returnKeyType {
|
||||
strongSelf.textNode.textView.returnKeyType = item.returnKeyType
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
if let currentText = strongSelf.textNode.attributedText {
|
||||
if currentText.string != attributedText.string || updatedTheme != nil {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
} else {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
|
||||
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
|
||||
}
|
||||
|
||||
strongSelf.textNode.keyboardAppearance = item.theme.keyboardAppearance
|
||||
|
||||
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0))
|
||||
|
||||
let _ = limitTextApply()
|
||||
strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: layout.contentSize.height - 15.0 - limitTextLayout.size.height), size: limitTextLayout.size)
|
||||
if limitTextString != nil {
|
||||
if strongSelf.limitTextNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.limitTextNode)
|
||||
}
|
||||
} else if strongSelf.limitTextNode.supernode != nil {
|
||||
strongSelf.limitTextNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
let inlineActionButtonNode: HighlightableButtonNode
|
||||
if let currentInlineActionButtonNode = strongSelf.inlineActionButtonNode {
|
||||
inlineActionButtonNode = currentInlineActionButtonNode
|
||||
} else {
|
||||
inlineActionButtonNode = HighlightableButtonNode()
|
||||
inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.theme.list.itemAccentColor), for: .normal)
|
||||
inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside)
|
||||
strongSelf.addSubnode(inlineActionButtonNode)
|
||||
strongSelf.inlineActionButtonNode = inlineActionButtonNode
|
||||
}
|
||||
inlineActionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - inlineAction.icon.size.width - 11.0, y: 7.0), size: inlineAction.icon.size)
|
||||
} else if let inlineActionButtonNode = strongSelf.inlineActionButtonNode {
|
||||
inlineActionButtonNode.removeFromSupernode()
|
||||
strongSelf.inlineActionButtonNode = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
super.animateFrameTransition(progress, currentValue)
|
||||
|
||||
guard let params = self.layoutParams else {
|
||||
return
|
||||
}
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
let insets = self.insets
|
||||
let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom))
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height - separatorHeight), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight))
|
||||
|
||||
self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset)))
|
||||
}
|
||||
|
||||
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.updatedFocus?(true)
|
||||
}
|
||||
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.updatedFocus?(false)
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if let item = self.item {
|
||||
if text.count > 1, let processPaste = item.processPaste {
|
||||
processPaste(text)
|
||||
return false
|
||||
}
|
||||
|
||||
if let action = item.action, text == "\n" {
|
||||
action()
|
||||
return false
|
||||
}
|
||||
|
||||
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||
if !item.shouldUpdateText(newText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let item = self.item {
|
||||
if let text = self.textNode.attributedText {
|
||||
let updatedText = text.string
|
||||
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
if text.string != updatedAttributedText.string {
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
}
|
||||
item.textUpdated(updatedText)
|
||||
} else {
|
||||
item.textUpdated("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
|
||||
if let _ = self.item {
|
||||
let text: String? = UIPasteboard.general.string
|
||||
if let _ = text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func focus() {
|
||||
if !self.textNode.textView.isFirstResponder {
|
||||
self.textNode.textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.textNode.layer.addShakeAnimation()
|
||||
}
|
||||
|
||||
@objc private func inlineActionPressed() {
|
||||
if let action = self.item?.inlineAction?.action {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,406 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListMultilineTextBaseFont {
|
||||
case `default`
|
||||
case monospace
|
||||
}
|
||||
|
||||
class ItemListMultilineTextItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let text: String
|
||||
let font: ItemListMultilineTextBaseFont
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let action: (() -> Void)?
|
||||
let longTapAction: (() -> Void)?
|
||||
let linkItemAction: ((String) -> Void)?
|
||||
|
||||
let tag: Any?
|
||||
|
||||
let selectable: Bool
|
||||
|
||||
init(theme: WalletTheme, text: String, font: ItemListMultilineTextBaseFont = .default, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((String) -> Void)? = nil, tag: Any? = nil) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
self.linkItemAction = linkItemAction
|
||||
self.tag = tag
|
||||
|
||||
self.selectable = action != nil
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListMultilineTextItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListMultilineTextItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action?()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
private let titleBoldFont = Font.medium(17.0)
|
||||
private let titleItalicFont = Font.italic(17.0)
|
||||
private let titleBoldItalicFont = Font.semiboldItalic(17.0)
|
||||
private let titleFixedFont = Font.regular(17.0)
|
||||
|
||||
class ItemListMultilineTextItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
private let textNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListMultilineTextItem?
|
||||
|
||||
var tag: Any? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
override var canBeLongTapped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.contentMode = .left
|
||||
self.textNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { [weak self] point in
|
||||
if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil {
|
||||
return .waitForSingleTap
|
||||
}
|
||||
return .fail
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateTouchesAtPoint(point)
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let textColor: UIColor = item.theme.list.itemPrimaryTextColor
|
||||
|
||||
let leftInset: CGFloat
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
leftInset = 16.0 + params.leftInset
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
leftInset = 16.0 + params.rightInset
|
||||
}
|
||||
|
||||
var baseFont = titleFont
|
||||
if case .monospace = item.font {
|
||||
baseFont = Font.monospace(17.0)
|
||||
}
|
||||
|
||||
let string = NSAttributedString(string: item.text, font: baseFont, textColor: textColor)
|
||||
|
||||
let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = item.text
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 16.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && self.linkItemAtPoint(point) == nil {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap, .longTap:
|
||||
if let item = self.item, let linkItem = self.linkItemAtPoint(location) {
|
||||
item.linkItemAction?(linkItem)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func linkItemAtPoint(_ point: CGPoint) -> String? {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (_, _) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func longTapped() {
|
||||
self.item?.longTapAction?()
|
||||
}
|
||||
|
||||
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||
if let item = self.item {
|
||||
var rects: [CGRect]?
|
||||
if let point = point {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let possibleNames: [String] = [
|
||||
]
|
||||
for name in possibleNames {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
||||
rects = self.textNode.attributeRects(name: name, at: index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5))
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
||||
}
|
||||
linkHighlightingNode.frame = self.textNode.frame
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import ActivityIndicator
|
||||
|
||||
enum ItemListSectionHeaderAccessoryTextColor {
|
||||
case generic
|
||||
case destructive
|
||||
}
|
||||
|
||||
struct ItemListSectionHeaderAccessoryText: Equatable {
|
||||
let value: String
|
||||
let color: ItemListSectionHeaderAccessoryTextColor
|
||||
let icon: UIImage?
|
||||
|
||||
init(value: String, color: ItemListSectionHeaderAccessoryTextColor, icon: UIImage? = nil) {
|
||||
self.value = value
|
||||
self.color = color
|
||||
self.icon = icon
|
||||
}
|
||||
}
|
||||
|
||||
enum ItemListSectionHeaderActivityIndicator {
|
||||
case none
|
||||
case left
|
||||
case right
|
||||
|
||||
fileprivate var hasActivity: Bool {
|
||||
switch self {
|
||||
case .left, .right:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let text: String
|
||||
let multiline: Bool
|
||||
let activityIndicator: ItemListSectionHeaderActivityIndicator
|
||||
let accessoryText: ItemListSectionHeaderAccessoryText?
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
let isAlwaysPlain: Bool = true
|
||||
|
||||
init(theme: WalletTheme, text: String, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.multiline = multiline
|
||||
self.activityIndicator = activityIndicator
|
||||
self.accessoryText = accessoryText
|
||||
self.sectionId = sectionId
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListSectionHeaderItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
guard let nodeValue = node() as? ItemListSectionHeaderItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(14.0)
|
||||
|
||||
class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
private var item: ItemListSectionHeaderItem?
|
||||
|
||||
private let titleNode: TextNode
|
||||
private let accessoryTextNode: TextNode
|
||||
private var accessoryImageNode: ASImageNode?
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.accessoryTextNode = TextNode()
|
||||
self.accessoryTextNode.isUserInteractionEnabled = false
|
||||
self.accessoryTextNode.contentMode = .left
|
||||
self.accessoryTextNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = [.staticText, .header]
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.accessoryTextNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode)
|
||||
|
||||
let previousItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 15.0 + params.leftInset
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var accessoryTextString: NSAttributedString?
|
||||
var accessoryIcon: UIImage?
|
||||
if let accessoryText = item.accessoryText {
|
||||
let color: UIColor
|
||||
switch accessoryText.color {
|
||||
case .generic:
|
||||
color = item.theme.list.sectionHeaderTextColor
|
||||
case .destructive:
|
||||
color = item.theme.list.freeTextErrorColor
|
||||
}
|
||||
accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color)
|
||||
accessoryIcon = accessoryText.icon
|
||||
}
|
||||
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
var insets = UIEdgeInsets()
|
||||
|
||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 13.0)
|
||||
switch neighbors.top {
|
||||
case .none:
|
||||
insets.top += 24.0
|
||||
case .otherSection:
|
||||
insets.top += 28.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = accessoryApply()
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = item.text
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size)
|
||||
|
||||
var accessoryTextOffset: CGFloat = 0.0
|
||||
if let accessoryIcon = accessoryIcon {
|
||||
accessoryTextOffset += accessoryIcon.size.width + 3.0
|
||||
}
|
||||
strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width - accessoryTextOffset, y: 7.0), size: accessoryLayout.size)
|
||||
|
||||
if let accessoryIcon = accessoryIcon {
|
||||
let accessoryImageNode: ASImageNode
|
||||
if let currentAccessoryImageNode = strongSelf.accessoryImageNode {
|
||||
accessoryImageNode = currentAccessoryImageNode
|
||||
} else {
|
||||
accessoryImageNode = ASImageNode()
|
||||
accessoryImageNode.displaysAsynchronously = false
|
||||
accessoryImageNode.displayWithoutProcessing = true
|
||||
strongSelf.addSubnode(accessoryImageNode)
|
||||
strongSelf.accessoryImageNode = accessoryImageNode
|
||||
}
|
||||
accessoryImageNode.image = accessoryIcon
|
||||
accessoryImageNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryIcon.size.width, y: 7.0), size: accessoryIcon.size)
|
||||
} else if let accessoryImageNode = strongSelf.accessoryImageNode {
|
||||
accessoryImageNode.removeFromSupernode()
|
||||
strongSelf.accessoryImageNode = nil
|
||||
}
|
||||
|
||||
if previousItem?.activityIndicator != item.activityIndicator {
|
||||
if item.activityIndicator.hasActivity {
|
||||
let activityIndicator: ActivityIndicator
|
||||
if let currentActivityIndicator = strongSelf.activityIndicator {
|
||||
activityIndicator = currentActivityIndicator
|
||||
} else {
|
||||
activityIndicator = ActivityIndicator(type: .custom(item.theme.list.sectionHeaderTextColor, 18.0, 1.0, false))
|
||||
strongSelf.addSubnode(activityIndicator)
|
||||
strongSelf.activityIndicator = activityIndicator
|
||||
}
|
||||
activityIndicator.isHidden = false
|
||||
if previousItem != nil {
|
||||
activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
} else if let activityIndicator = strongSelf.activityIndicator {
|
||||
activityIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { finished in
|
||||
if finished {
|
||||
activityIndicator.isHidden = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var activityIndicatorOrigin: CGPoint?
|
||||
switch item.activityIndicator {
|
||||
case .left:
|
||||
activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel)
|
||||
case .right:
|
||||
activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let activityIndicatorOrigin = activityIndicatorOrigin {
|
||||
strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -1,462 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListSingleLineInputItemType: Equatable {
|
||||
case regular(capitalization: Bool, autocorrection: Bool)
|
||||
case password
|
||||
case email
|
||||
case number
|
||||
case decimal
|
||||
case username
|
||||
}
|
||||
|
||||
enum ItemListSingleLineInputClearType: Equatable {
|
||||
case none
|
||||
case always
|
||||
case onFocus
|
||||
|
||||
var hasButton: Bool {
|
||||
switch self {
|
||||
case .none:
|
||||
return false
|
||||
case .always, .onFocus:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let strings: WalletStrings
|
||||
let title: NSAttributedString
|
||||
let text: String
|
||||
let placeholder: String
|
||||
let type: ItemListSingleLineInputItemType
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let spacing: CGFloat
|
||||
let clearType: ItemListSingleLineInputClearType
|
||||
let enabled: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
let textUpdated: (String) -> Void
|
||||
let shouldUpdateText: (String) -> Bool
|
||||
let processPaste: ((String) -> String)?
|
||||
let updatedFocus: ((Bool) -> Void)?
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: WalletTheme, strings: WalletStrings, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.placeholder = placeholder
|
||||
self.type = type
|
||||
self.returnKeyType = returnKeyType
|
||||
self.spacing = spacing
|
||||
self.clearType = clearType
|
||||
self.enabled = enabled
|
||||
self.tag = tag
|
||||
self.sectionId = sectionId
|
||||
self.textUpdated = textUpdated
|
||||
self.shouldUpdateText = shouldUpdateText
|
||||
self.processPaste = processPaste
|
||||
self.updatedFocus = updatedFocus
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListSingleLineInputItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListSingleLineInputItemNode {
|
||||
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextFieldNode
|
||||
private let clearIconNode: ASImageNode
|
||||
private let clearButtonNode: HighlightableButtonNode
|
||||
|
||||
private var item: ItemListSingleLineInputItem?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.textNode = TextFieldNode()
|
||||
|
||||
self.clearIconNode = ASImageNode()
|
||||
self.clearIconNode.isLayerBacked = true
|
||||
self.clearIconNode.displayWithoutProcessing = true
|
||||
self.clearIconNode.displaysAsynchronously = false
|
||||
|
||||
self.clearButtonNode = HighlightableButtonNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.clearIconNode)
|
||||
self.addSubnode(self.clearButtonNode)
|
||||
|
||||
self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.clearButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.clearIconNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.clearIconNode.alpha = 1.0
|
||||
strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0)]
|
||||
self.textNode.textField.font = Font.regular(17.0)
|
||||
if let item = self.item {
|
||||
self.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor
|
||||
self.textNode.textField.keyboardAppearance = item.theme.keyboardAppearance
|
||||
self.textNode.textField.tintColor = item.theme.list.itemAccentColor
|
||||
self.textNode.textField.accessibilityHint = item.placeholder
|
||||
}
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.textField.delegate = self
|
||||
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
var updatedClearIcon: UIImage?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
updatedClearIcon = itemListClearInputIcon(item.theme)
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
var rightInset: CGFloat = 16.0 + params.rightInset
|
||||
|
||||
if item.clearType.hasButton {
|
||||
rightInset += 32.0
|
||||
}
|
||||
|
||||
let titleString = NSMutableAttributedString(attributedString: item.title)
|
||||
titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length))
|
||||
titleString.addAttributes([NSAttributedString.Key.font: Font.regular(17.0)], range: NSMakeRange(0, titleString.length))
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: 44.0)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
|
||||
strongSelf.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor
|
||||
strongSelf.textNode.textField.keyboardAppearance = item.theme.keyboardAppearance
|
||||
strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
|
||||
let secureEntry: Bool
|
||||
let capitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let keyboardType: UIKeyboardType
|
||||
|
||||
switch item.type {
|
||||
case let .regular(capitalization, autocorrection):
|
||||
secureEntry = false
|
||||
capitalizationType = capitalization ? .sentences : .none
|
||||
autocorrectionType = autocorrection ? .default : .no
|
||||
keyboardType = .default
|
||||
case .email:
|
||||
secureEntry = false
|
||||
capitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
keyboardType = .emailAddress
|
||||
case .password:
|
||||
secureEntry = true
|
||||
capitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
keyboardType = .default
|
||||
case .number:
|
||||
secureEntry = false
|
||||
capitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
keyboardType = .asciiCapableNumberPad
|
||||
} else {
|
||||
keyboardType = .numberPad
|
||||
}
|
||||
case .decimal:
|
||||
secureEntry = false
|
||||
capitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
keyboardType = .decimalPad
|
||||
case .username:
|
||||
secureEntry = false
|
||||
capitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
keyboardType = .asciiCapable
|
||||
}
|
||||
|
||||
if strongSelf.textNode.textField.isSecureTextEntry != secureEntry {
|
||||
strongSelf.textNode.textField.isSecureTextEntry = secureEntry
|
||||
}
|
||||
if strongSelf.textNode.textField.keyboardType != keyboardType {
|
||||
strongSelf.textNode.textField.keyboardType = keyboardType
|
||||
}
|
||||
if strongSelf.textNode.textField.autocapitalizationType != capitalizationType {
|
||||
strongSelf.textNode.textField.autocapitalizationType = capitalizationType
|
||||
}
|
||||
if strongSelf.textNode.textField.autocorrectionType != autocorrectionType {
|
||||
strongSelf.textNode.textField.autocorrectionType = autocorrectionType
|
||||
}
|
||||
if strongSelf.textNode.textField.returnKeyType != item.returnKeyType {
|
||||
strongSelf.textNode.textField.returnKeyType = item.returnKeyType
|
||||
}
|
||||
|
||||
if let currentText = strongSelf.textNode.textField.text {
|
||||
if currentText != item.text {
|
||||
strongSelf.textNode.textField.text = item.text
|
||||
}
|
||||
} else {
|
||||
strongSelf.textNode.textField.text = item.text
|
||||
}
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: 40.0))
|
||||
|
||||
if let image = updatedClearIcon {
|
||||
strongSelf.clearIconNode.image = image
|
||||
}
|
||||
|
||||
let buttonSize = CGSize(width: 38.0, height: layout.contentSize.height)
|
||||
strongSelf.clearButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width, y: 0.0), size: buttonSize)
|
||||
if let image = strongSelf.clearIconNode.image {
|
||||
strongSelf.clearIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
strongSelf.updateClearButtonVisibility()
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) {
|
||||
strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText
|
||||
strongSelf.textNode.textField.accessibilityHint = attributedPlaceholderText.string
|
||||
}
|
||||
|
||||
strongSelf.textNode.isUserInteractionEnabled = item.enabled
|
||||
strongSelf.textNode.alpha = item.enabled ? 1.0 : 0.4
|
||||
|
||||
strongSelf.clearButtonNode.accessibilityLabel = item.strings.Wallet_VoiceOver_Editing_ClearText
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func updateClearButtonVisibility() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
let isHidden: Bool
|
||||
switch item.clearType {
|
||||
case .none:
|
||||
isHidden = true
|
||||
case .always:
|
||||
isHidden = item.text.isEmpty
|
||||
case .onFocus:
|
||||
isHidden = !self.textNode.textField.isFirstResponder || item.text.isEmpty
|
||||
}
|
||||
self.clearIconNode.isHidden = isHidden
|
||||
self.clearButtonNode.isHidden = isHidden
|
||||
self.clearButtonNode.isAccessibilityElement = isHidden
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func textFieldTextChanged(_ textField: UITextField) {
|
||||
self.textUpdated(self.textNode.textField.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func clearButtonPressed() {
|
||||
self.textNode.textField.text = ""
|
||||
self.textUpdated("")
|
||||
}
|
||||
|
||||
private func textUpdated(_ text: String) {
|
||||
self.item?.textUpdated(text)
|
||||
}
|
||||
|
||||
func focus() {
|
||||
if !self.textNode.textField.isFirstResponder {
|
||||
self.textNode.textField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if let item = self.item {
|
||||
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
if !item.shouldUpdateText(newText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if string.count > 1, let item = self.item, let processPaste = item.processPaste {
|
||||
let result = processPaste(string)
|
||||
if result != string {
|
||||
var text = textField.text ?? ""
|
||||
text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: result)
|
||||
textField.text = text
|
||||
if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + result.count) {
|
||||
let selectionRange = textField.textRange(from: startPosition, to: startPosition)
|
||||
DispatchQueue.main.async {
|
||||
textField.selectedTextRange = selectionRange
|
||||
}
|
||||
}
|
||||
self.textFieldTextChanged(textField)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.item?.action()
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
self.item?.updatedFocus?(true)
|
||||
self.updateClearButtonVisibility()
|
||||
}
|
||||
|
||||
@objc func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
self.item?.updatedFocus?(false)
|
||||
self.updateClearButtonVisibility()
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.textNode.layer.addShakeAnimation()
|
||||
}
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListSwitchItemNodeType {
|
||||
case regular
|
||||
case icon
|
||||
}
|
||||
|
||||
class ItemListSwitchItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let title: String
|
||||
let value: Bool
|
||||
let type: ItemListSwitchItemNodeType
|
||||
let enableInteractiveChanges: Bool
|
||||
let enabled: Bool
|
||||
let disableLeadingInset: Bool
|
||||
let maximumNumberOfLines: Int
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let updated: (Bool) -> Void
|
||||
let activatedWhileDisabled: () -> Void
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: WalletTheme, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.type = type
|
||||
self.enableInteractiveChanges = enableInteractiveChanges
|
||||
self.enabled = enabled
|
||||
self.disableLeadingInset = disableLeadingInset
|
||||
self.maximumNumberOfLines = maximumNumberOfLines
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.updated = updated
|
||||
self.activatedWhileDisabled = activatedWhileDisabled
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListSwitchItemNode(type: self.type)
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListSwitchItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
apply(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let titleNode: TextNode
|
||||
private var switchNode: SwitchNode
|
||||
private let switchGestureNode: ASDisplayNode
|
||||
private var disabledOverlayNode: ASDisplayNode?
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListSwitchItem?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init(type: ItemListSwitchItemNodeType) {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.switchNode = SwitchNode()
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.switchGestureNode = ASDisplayNode()
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.switchNode)
|
||||
self.addSubnode(self.switchGestureNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
self.activateArea.activate = { [weak self] in
|
||||
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
|
||||
return false
|
||||
}
|
||||
let value = !strongSelf.switchNode.isOn
|
||||
if item.enableInteractiveChanges {
|
||||
strongSelf.switchNode.setOn(value, animated: true)
|
||||
}
|
||||
item.updated(value)
|
||||
return true
|
||||
}
|
||||
|
||||
self.switchNode.valueUpdated = { [weak self] value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.updated(value)
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
var currentDisabledOverlayNode = self.disabledOverlayNode
|
||||
|
||||
return { item, params, neighbors in
|
||||
var contentSize: CGSize
|
||||
var insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let titleFont = Font.regular(17.0)
|
||||
|
||||
var updatedTheme: WalletTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 44.0)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 44.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
if item.disableLeadingInset {
|
||||
insets.top = 0.0
|
||||
insets.bottom = 0.0
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0)
|
||||
|
||||
if !item.enabled {
|
||||
if currentDisabledOverlayNode == nil {
|
||||
currentDisabledOverlayNode = ASDisplayNode()
|
||||
currentDisabledOverlayNode?.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6)
|
||||
}
|
||||
} else {
|
||||
currentDisabledOverlayNode = nil
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
strongSelf.activateArea.accessibilityValue = item.value ? "On" : "Off"
|
||||
strongSelf.activateArea.accessibilityHint = "Tap to change"
|
||||
var accessibilityTraits = UIAccessibilityTraits()
|
||||
if item.enabled {
|
||||
} else {
|
||||
accessibilityTraits.insert(.notEnabled)
|
||||
}
|
||||
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
|
||||
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
|
||||
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
|
||||
strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode)
|
||||
currentDisabledOverlayNode.alpha = 0.0
|
||||
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
|
||||
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
|
||||
} else {
|
||||
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
|
||||
}
|
||||
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
|
||||
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
|
||||
disabledOverlayNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.disabledOverlayNode = nil
|
||||
}
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 16.0 + params.leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
if let switchView = strongSelf.switchNode.view as? UISwitch {
|
||||
if strongSelf.switchNode.bounds.size.width.isZero {
|
||||
switchView.sizeToFit()
|
||||
}
|
||||
let switchSize = switchView.bounds.size
|
||||
|
||||
strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize)
|
||||
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
|
||||
if switchView.isOn != item.value {
|
||||
switchView.setOn(item.value, animated: animated)
|
||||
}
|
||||
switchView.isUserInteractionEnabled = item.enableInteractiveChanges
|
||||
}
|
||||
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
guard let item = self.item else {
|
||||
return false
|
||||
}
|
||||
if !item.enabled {
|
||||
return false
|
||||
}
|
||||
self.switchNode.isOn = !self.switchNode.isOn
|
||||
item.updated(self.switchNode.isOn)
|
||||
return true
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
|
||||
if item.enabled {
|
||||
let value = switchView.isOn
|
||||
item.updated(!value)
|
||||
} else {
|
||||
item.activatedWhileDisabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Markdown
|
||||
|
||||
enum ItemListTextItemText {
|
||||
case plain(String)
|
||||
case markdown(String)
|
||||
}
|
||||
|
||||
enum ItemListTextItemLinkAction {
|
||||
case tap(String)
|
||||
}
|
||||
|
||||
class ItemListTextItem: ListViewItem, ItemListItem {
|
||||
let theme: WalletTheme
|
||||
let text: ItemListTextItemText
|
||||
let sectionId: ItemListSectionId
|
||||
let linkAction: ((ItemListTextItemLinkAction) -> Void)?
|
||||
let style: ItemListStyle
|
||||
let isAlwaysPlain: Bool = true
|
||||
|
||||
init(theme: WalletTheme, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.sectionId = sectionId
|
||||
self.linkAction = linkAction
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListTextItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
guard let nodeValue = node() as? ItemListTextItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(14.0)
|
||||
private let titleBoldFont = Font.semibold(14.0)
|
||||
|
||||
class ItemListTextItemNode: ListViewItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListTextItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = .staticText
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { _ in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 15.0 + params.leftInset
|
||||
let verticalInset: CGFloat = 7.0
|
||||
|
||||
let attributedText: NSAttributedString
|
||||
switch item.text {
|
||||
case let .plain(text):
|
||||
attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.theme.list.freeTextColor)
|
||||
case let .markdown(text):
|
||||
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
}
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
|
||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = attributedText.string
|
||||
|
||||
strongSelf.accessibilityLabel = attributedText.string
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
let titleFrame = self.titleNode.frame
|
||||
if let item = self.item, titleFrame.contains(location) {
|
||||
if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
|
||||
item.linkAction?(.tap(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import AnimatedStickerNode
|
||||
|
||||
final class ToastNode: ASDisplayNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let effectView: UIView
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
init(theme: WalletTheme, animationPath: String, text: String) {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.cornerRadius = 9.0
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
if case .dark = theme.keyboardAppearance {
|
||||
self.backgroundNode.backgroundColor = theme.navigationBar.backgroundColor
|
||||
} else {
|
||||
self.backgroundNode.backgroundColor = .clear
|
||||
}
|
||||
|
||||
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||
self.backgroundNode.view.addSubview(self.effectView)
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
self.animationNode.visibility = true
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: animationPath), width: 100, height: 100, playbackMode: .once, mode: .direct)
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func update(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
let contentSideInset: CGFloat = 10.0
|
||||
let contentVerticalInset: CGFloat = 8.0
|
||||
let iconSpacing: CGFloat = 4.0
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentSideInset * 2.0, height: .greatestFiniteMagnitude))
|
||||
let iconSize = CGSize(width: 32.0, height: 32.0)
|
||||
|
||||
let contentSize = CGSize(width: iconSize.width + iconSpacing + textSize.width, height: max(iconSize.height, textSize.height))
|
||||
|
||||
let insets = layout.insets(options: .input)
|
||||
let contentOriginX = floor((layout.size.width - contentSize.width) / 2.0)
|
||||
let contentOriginY = insets.top + floor((layout.size.height - insets.top - insets.bottom - contentSize.height) / 2.0)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: contentOriginX, y: contentOriginY + floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.updateFrame(node: self.animationNode, frame: iconFrame)
|
||||
self.animationNode.updateLayout(size: iconFrame.size)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + iconSpacing, y: contentOriginY + floor((contentSize.height - textSize.height) / 2.0)), size: textSize)
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: contentOriginX - contentSideInset, y: contentOriginY - contentVerticalInset), size: CGSize(width: contentSize.width + contentSideInset * 2.0, height: contentSize.height + contentVerticalInset * 2.0))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func show(removed: @escaping () -> Void) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 3.0, removeOnCompletion: false, completion: { _ in
|
||||
removed()
|
||||
})
|
||||
}
|
||||
}
|