Assorted updates and bug fixes

This commit is contained in:
Ali 2020-11-10 21:04:52 +04:00
commit d8406daa67
2556 changed files with 1593872 additions and 329995 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import UIKit
@objc(Application) class Application: UIApplication {
}

View File

@ -1,7 +0,0 @@
#import <UIKit/UIKit.h>
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, @"Application", @"AppDelegate");
}
}

View File

@ -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" = "Youre 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" = "Lets 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";

View File

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

View File

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

View File

@ -13,8 +13,11 @@ objc_library(
includes = [
"PublicHeaders",
],
copts = [
"-DTELEGRAM_USE_BORINGSSL=1",
],
deps = [
"//submodules/openssl:openssl",
"//third-party/boringssl:crypto",
],
sdk_frameworks = [
"Foundation",

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View File

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

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

View File

@ -17,7 +17,7 @@ objc_library(
],
deps = [
"//submodules/SSignalKit/SSignalKit:SSignalKit",
"//submodules/openssl:openssl",
"//third-party/boringssl:crypto",
"//submodules/ton:ton",
],
visibility = [

View File

@ -14,7 +14,6 @@ swift_library(
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/WalletUrl:WalletUrl",
],
visibility = [
"//visibility:public",

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More