mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
[WIP] Message effects
This commit is contained in:
parent
fe8c2d8c15
commit
7ef63a81df
2
Tests/LottieMetalMacTest/.gitignore
vendored
Normal file
2
Tests/LottieMetalMacTest/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
TestData/*.json
|
||||
|
173
Tests/LottieMetalMacTest/BUILD
Normal file
173
Tests/LottieMetalMacTest/BUILD
Normal file
@ -0,0 +1,173 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl",
|
||||
"macos_application",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl",
|
||||
"swift_library",
|
||||
)
|
||||
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
|
||||
load(
|
||||
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||
"apple_resource_bundle",
|
||||
"apple_resource_group",
|
||||
)
|
||||
|
||||
load(
|
||||
"@rules_xcodeproj//xcodeproj:defs.bzl",
|
||||
"top_level_target",
|
||||
"top_level_targets",
|
||||
"xcodeproj",
|
||||
"xcode_provisioning_profile",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
|
||||
|
||||
load(
|
||||
"@build_configuration//:variables.bzl",
|
||||
"telegram_bazel_path",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "AppResources",
|
||||
srcs = glob([
|
||||
"Resources/**/*",
|
||||
], exclude = ["Resources/**/.*"]),
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "BuildNumberInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
"""
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "VersionInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
"""
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "AppNameInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Test</string>
|
||||
"""
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "MacAppInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Telegram</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
"""
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "TestDataBundleFiles",
|
||||
srcs = glob([
|
||||
"TestData/*.json",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "TestDataBundleInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.telegram.TestDataBundle</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TestDataBundle</string>
|
||||
"""
|
||||
)
|
||||
|
||||
apple_resource_bundle(
|
||||
name = "TestDataBundle",
|
||||
infoplists = [
|
||||
":TestDataBundleInfoPlist",
|
||||
],
|
||||
resources = [
|
||||
":TestDataBundleFiles",
|
||||
],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "MacLib",
|
||||
srcs = glob([
|
||||
"MacSources/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
"Resources/Main.storyboard",
|
||||
],
|
||||
)
|
||||
|
||||
macos_application(
|
||||
name = "LottieMetalMacTest",
|
||||
app_icons = [],
|
||||
bundle_id = "com.example.hello-world-swift",
|
||||
infoplists = [
|
||||
":MacAppInfoPlist",
|
||||
":BuildNumberInfoPlist",
|
||||
":VersionInfoPlist",
|
||||
],
|
||||
minimum_os_version = "10.13",
|
||||
deps = [
|
||||
":MacLib"
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
xcodeproj(
|
||||
name = "LottieMetalMacTest_xcodeproj",
|
||||
build_mode = "bazel",
|
||||
bazel_path = telegram_bazel_path,
|
||||
project_name = "LottieMetalMacTest",
|
||||
tags = ["manual"],
|
||||
top_level_targets = top_level_targets(
|
||||
labels = [
|
||||
":LottieMetalMacTest",
|
||||
],
|
||||
),
|
||||
xcode_configurations = {
|
||||
"Debug": {
|
||||
"//command_line_option:compilation_mode": "dbg",
|
||||
},
|
||||
"Release": {
|
||||
"//command_line_option:compilation_mode": "opt",
|
||||
},
|
||||
},
|
||||
default_xcode_configuration = "Debug"
|
||||
)
|
19
Tests/LottieMetalMacTest/MacSources/AppDelegate.swift
Normal file
19
Tests/LottieMetalMacTest/MacSources/AppDelegate.swift
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Cocoa
|
||||
|
||||
@NSApplicationMain
|
||||
@objc(AppDelegate)
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {}
|
11
Tests/LottieMetalMacTest/MacSources/ViewController.swift
Normal file
11
Tests/LottieMetalMacTest/MacSources/ViewController.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Cocoa
|
||||
|
||||
@objc(ViewController)
|
||||
class ViewController: NSViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.layer?.backgroundColor = NSColor.blue.cgColor
|
||||
}
|
||||
}
|
||||
|
697
Tests/LottieMetalMacTest/Resources/Main.storyboard
Normal file
697
Tests/LottieMetalMacTest/Resources/Main.storyboard
Normal file
@ -0,0 +1,697 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="Hello World" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Hello World" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About Hello World" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide Hello World" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit Hello World" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="dMs-cI-mzQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||
<items>
|
||||
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
|
||||
<connections>
|
||||
<action selector="newDocument:" target="Ady-hI-5gd" id="4Si-XN-c54"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
|
||||
<connections>
|
||||
<action selector="openDocument:" target="Ady-hI-5gd" id="bVn-NM-KNZ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open Recent" id="tXI-mr-wws">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
|
||||
<items>
|
||||
<menuItem title="Clear Menu" id="vNY-rz-j42">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="Daa-9d-B3U"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||
<connections>
|
||||
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="Ady-hI-5gd" id="teZ-XB-qJY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
|
||||
<connections>
|
||||
<action selector="saveDocumentAs:" target="Ady-hI-5gd" id="mDf-zr-I0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
|
||||
<connections>
|
||||
<action selector="revertDocumentToSaved:" target="Ady-hI-5gd" id="iJ3-Pv-kwq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
|
||||
<connections>
|
||||
<action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Format" id="jxT-CU-nIS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
|
||||
<items>
|
||||
<menuItem title="Font" id="Gi5-1S-RQB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
|
||||
<items>
|
||||
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq"/>
|
||||
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27"/>
|
||||
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq"/>
|
||||
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
|
||||
<connections>
|
||||
<action selector="underline:" target="Ady-hI-5gd" id="FYS-2b-JAY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
|
||||
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL"/>
|
||||
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST"/>
|
||||
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
|
||||
<menuItem title="Kern" id="jBQ-r6-VK2">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="GUa-eO-cwY">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardKerning:" target="Ady-hI-5gd" id="6dk-9l-Ckg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="cDB-IK-hbR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffKerning:" target="Ady-hI-5gd" id="U8a-gz-Maa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Tighten" id="46P-cB-AYj">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="tightenKerning:" target="Ady-hI-5gd" id="hr7-Nz-8ro"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Loosen" id="ogc-rX-tC1">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="loosenKerning:" target="Ady-hI-5gd" id="8i4-f9-FKE"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Ligatures" id="o6e-r0-MWq">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="agt-UL-0e3">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardLigatures:" target="Ady-hI-5gd" id="7uR-wd-Dx6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="J7y-lM-qPV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffLigatures:" target="Ady-hI-5gd" id="iX2-gA-Ilz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use All" id="xQD-1f-W4t">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useAllLigatures:" target="Ady-hI-5gd" id="KcB-kA-TuK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Baseline" id="OaQ-X3-Vso">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="3Om-Ey-2VK">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unscript:" target="Ady-hI-5gd" id="0vZ-95-Ywn"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Superscript" id="Rqc-34-cIF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="superscript:" target="Ady-hI-5gd" id="3qV-fo-wpU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Subscript" id="I0S-gh-46l">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="subscript:" target="Ady-hI-5gd" id="Q6W-4W-IGz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Raise" id="2h7-ER-AoG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="raiseBaseline:" target="Ady-hI-5gd" id="4sk-31-7Q9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Lower" id="1tx-W0-xDw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowerBaseline:" target="Ady-hI-5gd" id="OF1-bc-KW4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
|
||||
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
|
||||
<connections>
|
||||
<action selector="orderFrontColorPanel:" target="Ady-hI-5gd" id="mSX-Xz-DV3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
|
||||
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyFont:" target="Ady-hI-5gd" id="GJO-xA-L4q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteFont:" target="Ady-hI-5gd" id="JfD-CL-leO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Text" id="Fal-I4-PZk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Text" id="d9c-me-L2H">
|
||||
<items>
|
||||
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
|
||||
<connections>
|
||||
<action selector="alignLeft:" target="Ady-hI-5gd" id="zUv-R1-uAa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
|
||||
<connections>
|
||||
<action selector="alignCenter:" target="Ady-hI-5gd" id="spX-mk-kcS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Justify" id="J5U-5w-g23">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="alignJustified:" target="Ady-hI-5gd" id="ljL-7U-jND"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
|
||||
<connections>
|
||||
<action selector="alignRight:" target="Ady-hI-5gd" id="r48-bG-YeY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
|
||||
<menuItem title="Writing Direction" id="H1b-Si-o9J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
|
||||
<items>
|
||||
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="YGs-j5-SAR">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionNatural:" target="Ady-hI-5gd" id="qtV-5e-UBP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="Lbh-J2-qVU">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="S0X-9S-QSf"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="jFq-tB-4Kx">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="5fk-qB-AqJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
|
||||
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="Nop-cj-93Q">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionNatural:" target="Ady-hI-5gd" id="lPI-Se-ZHp"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="BgM-ve-c93">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="caW-Bv-w94"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="RB4-Sm-HuC">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="EXD-6r-ZUu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
|
||||
<menuItem title="Show Ruler" id="vLm-3I-IUL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleRuler:" target="Ady-hI-5gd" id="FOx-HJ-KwY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyRuler:" target="Ady-hI-5gd" id="71i-fW-3W2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteRuler:" target="Ady-hI-5gd" id="cSh-wd-qM2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="Hello World Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="4lI-sl-XQw"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
@ -161,6 +161,7 @@ func buildAnimationFolderItems(basePath: String, path: String) -> [(String, Stri
|
||||
return result
|
||||
}
|
||||
|
||||
@available (iOS 13.0, *)
|
||||
private func processAnimationFolderItems(items: [(String, String)], countPerBucket: Int, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
||||
let bucketCount = items.count / countPerBucket
|
||||
var buckets: [[(String, String)]] = []
|
||||
@ -253,6 +254,7 @@ private func processAnimationFolderItemsParallel(items: [(String, String)], stop
|
||||
return result
|
||||
}
|
||||
|
||||
@available (iOS 13.0, *)
|
||||
func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
||||
let items = buildAnimationFolderItems(basePath: basePath, path: path)
|
||||
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)
|
||||
|
@ -110,6 +110,8 @@ public final class ViewController: UIViewController {
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
SharedDisplayLinkDriver.shared.updateForegroundState(true)
|
||||
|
||||
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
||||
let filePath = bundlePath + "/fireworks.json"
|
||||
|
||||
@ -117,12 +119,15 @@ public final class ViewController: UIViewController {
|
||||
|
||||
self.view.layer.addSublayer(MetalEngine.shared.rootLayer)
|
||||
|
||||
if "".isEmpty {
|
||||
if !"".isEmpty {
|
||||
if #available(iOS 13.0, *) {
|
||||
self.test = ReferenceCompareTest(view: self.view)
|
||||
}
|
||||
} else if !"".isEmpty {
|
||||
let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||
} else if "".isEmpty {
|
||||
let cachedAnimation = cacheLottieMetalAnimation(path: filePath)!
|
||||
let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)!
|
||||
|
||||
/*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||
|
||||
var startTime = CFAbsoluteTimeGetCurrent()
|
||||
let animation = LottieAnimation(data: animationData)!
|
||||
@ -131,9 +136,9 @@ public final class ViewController: UIViewController {
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
let animationContainer = LottieAnimationContainer(animation: animation)
|
||||
animationContainer.update(0)
|
||||
print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")*/
|
||||
|
||||
let lottieLayer = LottieContentLayer(animation: animationContainer)
|
||||
let lottieLayer = LottieContentLayer(content: animation)
|
||||
lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0))
|
||||
self.view.layer.addSublayer(lottieLayer)
|
||||
lottieLayer.setNeedsUpdate()
|
||||
@ -162,7 +167,7 @@ public final class ViewController: UIViewController {
|
||||
var frameIndex = 0
|
||||
while true {
|
||||
animationContainer.update(frameIndex)
|
||||
let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false)
|
||||
//let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false)
|
||||
frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount
|
||||
numUpdates += 1
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
|
@ -168,7 +168,7 @@ public protocol AnimatedStickerNode: ASDisplayNode {
|
||||
var visibility: Bool { get set }
|
||||
var overrideVisibility: Bool { get set }
|
||||
|
||||
var isPlayingChanged: (Bool) -> Void { get }
|
||||
var isPlayingChanged: (Bool) -> Void { get set }
|
||||
|
||||
func cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
|
||||
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
|
||||
|
@ -39,6 +39,8 @@ swift_library(
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
|
||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,8 @@ import ShimmerEffect
|
||||
import TextFormat
|
||||
import LegacyMessageInputPanel
|
||||
import LegacyMessageInputPanelInputView
|
||||
import ReactionSelectionNode
|
||||
import TopMessageReactions
|
||||
|
||||
private let buttonSize = CGSize(width: 88.0, height: 49.0)
|
||||
private let smallButtonWidth: CGFloat = 69.0
|
||||
@ -926,9 +928,31 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
if case .media = strongSelf.presentationInterfaceState.inputMode {
|
||||
hasEntityKeyboard = true
|
||||
}
|
||||
let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
|
||||
|
||||
let effectItems: Signal<[ReactionItem]?, NoError>
|
||||
if strongSelf.presentationInterfaceState.chatLocation.peerId != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser {
|
||||
effectItems = effectMessageReactions(context: strongSelf.context)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
effectItems = .single(nil)
|
||||
}
|
||||
|
||||
let availableMessageEffects = strongSelf.context.availableMessageEffects |> take(1)
|
||||
let hasPremium = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|
||||
let _ = (combineLatest(
|
||||
strongSelf.context.account.viewTracker.peerView(peerId) |> take(1),
|
||||
effectItems,
|
||||
availableMessageEffects,
|
||||
hasPremium
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView, effectItems, availableMessageEffects, hasPremium in
|
||||
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
|
||||
return
|
||||
}
|
||||
@ -955,7 +979,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
}, schedule: { [weak textInputPanelNode] _ in
|
||||
textInputPanelNode?.sendMessage(.schedule)
|
||||
})
|
||||
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
|
||||
strongSelf.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}, openScheduledMessages: {
|
||||
|
@ -492,7 +492,7 @@ public func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: Princ
|
||||
}
|
||||
|
||||
public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
||||
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||
public private(set) var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||
|
||||
private var currentType: ChatMessageBackgroundType?
|
||||
private var currentMaskMode: Bool?
|
||||
|
@ -33,7 +33,10 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/LottieMetal",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
"//submodules/ActivityIndicator",
|
||||
"//submodules/UndoUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -210,7 +210,9 @@ public func makeChatSendMessageActionSheetController(
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]? = nil
|
||||
reactionItems: [ReactionItem]? = nil,
|
||||
availableMessageEffects: AvailableMessageEffects? = nil,
|
||||
isPremium: Bool = false
|
||||
) -> ChatSendMessageActionSheetController {
|
||||
if textInputView.text.isEmpty {
|
||||
return ChatSendMessageActionSheetControllerImpl(
|
||||
@ -229,7 +231,7 @@ public func makeChatSendMessageActionSheetController(
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
reactionItems: nil
|
||||
)
|
||||
}
|
||||
|
||||
@ -250,6 +252,8 @@ public func makeChatSendMessageActionSheetController(
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
reactionItems: reactionItems,
|
||||
availableMessageEffects: availableMessageEffects,
|
||||
isPremium: isPremium
|
||||
)
|
||||
}
|
||||
|
@ -388,7 +388,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||
selectedItems: Set(),
|
||||
title: "Add an animated effect",
|
||||
reactionsLocked: false,
|
||||
|
@ -18,6 +18,9 @@ import ReactionSelectionNode
|
||||
import EntityKeyboard
|
||||
import LottieMetal
|
||||
import TelegramAnimatedStickerNode
|
||||
import AnimatedStickerNode
|
||||
import ChatInputTextNode
|
||||
import UndoUI
|
||||
|
||||
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||
@ -48,6 +51,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||
let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||
let reactionItems: [ReactionItem]?
|
||||
let availableMessageEffects: AvailableMessageEffects?
|
||||
let isPremium: Bool
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -65,7 +70,9 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]?
|
||||
reactionItems: [ReactionItem]?,
|
||||
availableMessageEffects: AvailableMessageEffects?,
|
||||
isPremium: Bool
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
@ -83,6 +90,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
self.sendMessage = sendMessage
|
||||
self.schedule = schedule
|
||||
self.reactionItems = reactionItems
|
||||
self.availableMessageEffects = availableMessageEffects
|
||||
self.isPremium = isPremium
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
||||
@ -115,7 +124,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
final class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
|
||||
private var sendButton: HighlightTrackingButton?
|
||||
private var sendButton: SendButton?
|
||||
private var messageItemView: MessageItemView?
|
||||
private var actionsStackNode: ContextControllerActionsStackNode?
|
||||
private var reactionContextNode: ReactionContextNode?
|
||||
@ -129,7 +138,10 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
|
||||
private let messageEffectDisposable = MetaDisposable()
|
||||
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
|
||||
private var standaloneReactionAnimation: LottieMetalAnimatedStickerNode?
|
||||
private var standaloneReactionAnimation: AnimatedStickerNode?
|
||||
|
||||
private var isLoadingEffectAnimation: Bool = false
|
||||
private var loadEffectAnimationDisposable: Disposable?
|
||||
|
||||
private var presentationAnimationState: PresentationAnimationState = .initial
|
||||
private var appliedAnimationState: PresentationAnimationState = .initial
|
||||
@ -164,6 +176,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
|
||||
deinit {
|
||||
self.messageEffectDisposable.dispose()
|
||||
self.loadEffectAnimationDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) {
|
||||
@ -195,6 +208,20 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
private func requestUpdateOverlayWantsToBeBelowKeyboard(transition: ContainedViewLayoutTransition) {
|
||||
guard let controller = self.environment?.controller() as? ChatSendMessageContextScreen else {
|
||||
return
|
||||
}
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
|
||||
func wantsToBeBelowKeyboard() -> Bool {
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
return reactionContextNode.wantsDisplayBelowKeyboard()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
@ -254,23 +281,32 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
)
|
||||
}
|
||||
|
||||
let sendButton: HighlightTrackingButton
|
||||
let sendButton: SendButton
|
||||
if let current = self.sendButton {
|
||||
sendButton = current
|
||||
} else {
|
||||
sendButton = HighlightTrackingButton()
|
||||
sendButton = SendButton()
|
||||
sendButton.accessibilityLabel = environment.strings.MediaPicker_Send
|
||||
sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside)
|
||||
if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) {
|
||||
/*if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.isUserInteractionEnabled = false
|
||||
sendButton.addSubview(snapshotView)
|
||||
}
|
||||
}*/
|
||||
self.sendButton = sendButton
|
||||
self.addSubview(sendButton)
|
||||
}
|
||||
|
||||
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
|
||||
sendButton.update(
|
||||
context: component.context,
|
||||
presentationData: presentationData,
|
||||
backgroundNode: component.wallpaperBackgroundNode,
|
||||
isLoadingEffectAnimation: self.isLoadingEffectAnimation,
|
||||
size: sourceSendButtonFrame.size,
|
||||
transition: transition
|
||||
)
|
||||
|
||||
let sendButtonScale: CGFloat
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
@ -279,55 +315,6 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
sendButtonScale = 1.0
|
||||
}
|
||||
|
||||
let messageItemView: MessageItemView
|
||||
if let current = self.messageItemView {
|
||||
messageItemView = current
|
||||
} else {
|
||||
messageItemView = MessageItemView(frame: CGRect())
|
||||
self.messageItemView = messageItemView
|
||||
self.addSubview(messageItemView)
|
||||
}
|
||||
|
||||
let textString: NSAttributedString
|
||||
if let attributedText = component.textInputView.attributedText {
|
||||
textString = attributedText
|
||||
} else {
|
||||
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
|
||||
}
|
||||
|
||||
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
|
||||
|
||||
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
|
||||
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
|
||||
let explicitMessageBackgroundSize: CGSize?
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
case .animatedOut:
|
||||
if self.animateOutToEmpty {
|
||||
explicitMessageBackgroundSize = nil
|
||||
} else {
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
}
|
||||
case .animatedIn:
|
||||
explicitMessageBackgroundSize = nil
|
||||
}
|
||||
|
||||
let messageTextInsets = sourceMessageTextInsets
|
||||
|
||||
let messageItemSize = messageItemView.update(
|
||||
context: component.context,
|
||||
presentationData: presentationData,
|
||||
backgroundNode: component.wallpaperBackgroundNode,
|
||||
textString: textString,
|
||||
textInsets: messageTextInsets,
|
||||
explicitBackgroundSize: explicitMessageBackgroundSize,
|
||||
maxTextWidth: localSourceTextInputViewFrame.width - 32.0,
|
||||
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
||||
transition: transition
|
||||
)
|
||||
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
|
||||
|
||||
let actionsStackNode: ContextControllerActionsStackNode
|
||||
if let current = self.actionsStackNode {
|
||||
actionsStackNode = current
|
||||
@ -430,7 +417,73 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
presentation: .modal,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
|
||||
|
||||
let messageItemView: MessageItemView
|
||||
if let current = self.messageItemView {
|
||||
messageItemView = current
|
||||
} else {
|
||||
messageItemView = MessageItemView(frame: CGRect())
|
||||
self.messageItemView = messageItemView
|
||||
self.addSubview(messageItemView)
|
||||
}
|
||||
|
||||
let textString: NSAttributedString
|
||||
if let attributedText = component.textInputView.attributedText {
|
||||
textString = attributedText
|
||||
} else {
|
||||
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
|
||||
}
|
||||
|
||||
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
|
||||
|
||||
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
|
||||
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
|
||||
let explicitMessageBackgroundSize: CGSize?
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
case .animatedOut:
|
||||
if self.animateOutToEmpty {
|
||||
explicitMessageBackgroundSize = nil
|
||||
} else {
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
}
|
||||
case .animatedIn:
|
||||
explicitMessageBackgroundSize = nil
|
||||
}
|
||||
|
||||
let messageTextInsets = sourceMessageTextInsets
|
||||
|
||||
var maxTextHeight: CGFloat = availableSize.height - 8.0
|
||||
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
|
||||
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isExpanded {
|
||||
maxTextHeight -= 300.0 + 8.0
|
||||
} else {
|
||||
maxTextHeight -= 60.0 + 14.0
|
||||
}
|
||||
}
|
||||
maxTextHeight -= environment.statusBarHeight + 14.0
|
||||
if environment.inputHeight != 0.0 {
|
||||
maxTextHeight -= environment.inputHeight
|
||||
} else {
|
||||
maxTextHeight -= actionsStackSize.height
|
||||
maxTextHeight -= environment.safeInsets.bottom
|
||||
}
|
||||
|
||||
let messageItemSize = messageItemView.update(
|
||||
context: component.context,
|
||||
presentationData: presentationData,
|
||||
backgroundNode: component.wallpaperBackgroundNode,
|
||||
textString: textString,
|
||||
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
||||
textInsets: messageTextInsets,
|
||||
explicitBackgroundSize: explicitMessageBackgroundSize,
|
||||
maxTextWidth: localSourceTextInputViewFrame.width,
|
||||
maxTextHeight: maxTextHeight,
|
||||
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
||||
transition: transition
|
||||
)
|
||||
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
|
||||
|
||||
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
|
||||
let reactionContextNode: ReactionContextNode
|
||||
@ -442,7 +495,21 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { item in
|
||||
var icon: EmojiPagerContentComponent.Item.Icon = .none
|
||||
if !component.isPremium, case let .custom(sourceEffectId) = item.reaction.rawValue, let availableMessageEffects = component.availableMessageEffects {
|
||||
for messageEffect in availableMessageEffects.messageEffects {
|
||||
if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId {
|
||||
if messageEffect.isPremium {
|
||||
icon = .locked
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ReactionContextItem.reaction(item: item, icon: icon)
|
||||
},
|
||||
selectedItems: Set(),
|
||||
title: "Add an animated effect",
|
||||
reactionsLocked: false,
|
||||
@ -477,9 +544,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
self.requestUpdateOverlayWantsToBeBelowKeyboard(transition: transition)
|
||||
}
|
||||
)
|
||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||
@ -506,11 +571,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.messageEffectDisposable.set((combineLatest(
|
||||
messageEffect,
|
||||
ReactionContextNode.randomGenericReactionEffect(context: component.context)
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
|
||||
self.messageEffectDisposable.set((messageEffect
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
@ -523,6 +585,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
if selectedMessageEffect.id == effectId {
|
||||
self.selectedMessageEffect = nil
|
||||
reactionContextNode.selectedItems = Set([])
|
||||
self.loadEffectAnimationDisposable?.dispose()
|
||||
self.isLoadingEffectAnimation = false
|
||||
|
||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||
self.standaloneReactionAnimation = nil
|
||||
@ -541,6 +605,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
HapticFeedback().tap()
|
||||
}
|
||||
} else {
|
||||
self.selectedMessageEffect = messageEffect
|
||||
@ -548,10 +614,14 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
HapticFeedback().tap()
|
||||
}
|
||||
|
||||
guard let targetView = self.messageItemView?.effectIconView else {
|
||||
return
|
||||
self.loadEffectAnimationDisposable?.dispose()
|
||||
self.isLoadingEffectAnimation = true
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||
@ -561,56 +631,141 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
let _ = path
|
||||
|
||||
var customEffectResource: MediaResource?
|
||||
var customEffectResource: (FileMediaReference, MediaResource)?
|
||||
if let effectAnimation = messageEffect.effectAnimation {
|
||||
customEffectResource = effectAnimation.resource
|
||||
customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource)
|
||||
} else {
|
||||
let effectSticker = messageEffect.effectSticker
|
||||
if let effectFile = effectSticker.videoThumbnails.first {
|
||||
customEffectResource = effectFile.resource
|
||||
customEffectResource = (FileMediaReference.standalone(media: effectSticker), effectFile.resource)
|
||||
}
|
||||
}
|
||||
guard let customEffectResource else {
|
||||
guard let (customEffectResourceFileReference, customEffectResource) = customEffectResource else {
|
||||
return
|
||||
}
|
||||
|
||||
let standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
|
||||
standaloneReactionAnimation.isUserInteractionEnabled = false
|
||||
let effectSize = CGSize(width: 380.0, height: 380.0)
|
||||
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
|
||||
effectFrame.origin.x -= effectFrame.width * 0.3
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
standaloneReactionAnimation.frame = effectFrame
|
||||
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
|
||||
self.addSubnode(standaloneReactionAnimation)
|
||||
|
||||
let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id)
|
||||
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
|
||||
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
||||
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let standaloneReactionAnimation {
|
||||
standaloneReactionAnimation.removeFromSupernode()
|
||||
if self.standaloneReactionAnimation === standaloneReactionAnimation {
|
||||
self.standaloneReactionAnimation = nil
|
||||
let context = component.context
|
||||
var loadEffectAnimationSignal: Signal<Never, NoError>
|
||||
loadEffectAnimationSignal = Signal { subscriber in
|
||||
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start()
|
||||
|
||||
let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource)
|
||||
|> filter { status in
|
||||
if status == .Local {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> take(1)).start(next: { _ in
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposabke.dispose()
|
||||
}
|
||||
}
|
||||
standaloneReactionAnimation.visibility = true
|
||||
#if DEBUG
|
||||
loadEffectAnimationSignal = loadEffectAnimationSignal |> delay(1.0, queue: .mainQueue())
|
||||
#endif
|
||||
|
||||
self.loadEffectAnimationDisposable = (loadEffectAnimationSignal
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingEffectAnimation = false
|
||||
|
||||
guard let targetView = self.messageItemView?.effectIconView else {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let standaloneReactionAnimation: AnimatedStickerNode
|
||||
#if targetEnvironment(simulator)
|
||||
standaloneReactionAnimation = DirectAnimatedStickerNode()
|
||||
#else
|
||||
standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
|
||||
#endif
|
||||
|
||||
standaloneReactionAnimation.isUserInteractionEnabled = false
|
||||
let effectSize = CGSize(width: 380.0, height: 380.0)
|
||||
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
|
||||
effectFrame.origin.x -= effectFrame.width * 0.3
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
standaloneReactionAnimation.frame = effectFrame
|
||||
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
|
||||
self.addSubnode(standaloneReactionAnimation)
|
||||
|
||||
let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id)
|
||||
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
|
||||
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
||||
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let standaloneReactionAnimation {
|
||||
standaloneReactionAnimation.removeFromSupernode()
|
||||
if self.standaloneReactionAnimation === standaloneReactionAnimation {
|
||||
self.standaloneReactionAnimation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
standaloneReactionAnimation.visibility = true
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
reactionContextNode.premiumReactionsSelected = { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
//TODO:localize
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
self.environment?.controller()?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .premiumPaywall(
|
||||
title: nil,
|
||||
text: "Subscribe to [TelegramPremium]() to add this animated effect.",
|
||||
customUndoText: nil,
|
||||
timeout: nil,
|
||||
linkAction: nil
|
||||
),
|
||||
elevatedLayout: false,
|
||||
action: { [weak self] action in
|
||||
guard let self, let component = self.component else {
|
||||
return false
|
||||
}
|
||||
if case .info = action {
|
||||
self.window?.endEditing(true)
|
||||
|
||||
//TODO:localize
|
||||
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .animatedEmoji, forceDark: false, dismissed: nil)
|
||||
let _ = premiumController
|
||||
//parentNavigationController.pushViewController(premiumController)
|
||||
}
|
||||
return false
|
||||
}
|
||||
), in: .current)
|
||||
}
|
||||
reactionContextNode.displayTail = true
|
||||
reactionContextNode.forceTailToRight = false
|
||||
reactionContextNode.forceDark = false
|
||||
reactionContextNode.isMessageEffects = true
|
||||
self.reactionContextNode = reactionContextNode
|
||||
self.addSubview(reactionContextNode.view)
|
||||
}
|
||||
}
|
||||
|
||||
let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
|
||||
|
||||
var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX, y: sourceSendButtonFrame.minY), size: sourceSendButtonFrame.size)
|
||||
var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 8.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize)
|
||||
var readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
|
||||
@ -622,6 +777,13 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
readySendButtonFrame.origin.y -= bottomOverflow
|
||||
}
|
||||
|
||||
let inputCoverOverflow = readyMessageItemFrame.maxY + 7.0 - (availableSize.height - environment.inputHeight)
|
||||
if inputCoverOverflow > 0.0 {
|
||||
readyMessageItemFrame.origin.y -= inputCoverOverflow
|
||||
readyActionsStackFrame.origin.y -= inputCoverOverflow
|
||||
readySendButtonFrame.origin.y -= inputCoverOverflow
|
||||
}
|
||||
|
||||
let messageItemFrame: CGRect
|
||||
let actionsStackFrame: CGRect
|
||||
let sendButtonFrame: CGRect
|
||||
@ -697,6 +859,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
|
||||
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
|
||||
transition.setScale(view: sendButton, scale: sendButtonScale)
|
||||
sendButton.updateGlobalRect(rect: sendButtonFrame, within: availableSize, transition: transition)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||
@ -769,6 +932,14 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
||||
private var processedDidAppear: Bool = false
|
||||
private var processedDidDisappear: Bool = false
|
||||
|
||||
override public var overlayWantsToBeBelowKeyboard: Bool {
|
||||
if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View {
|
||||
return componentView.wantsToBeBelowKeyboard()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
@ -786,7 +957,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]?
|
||||
reactionItems: [ReactionItem]?,
|
||||
availableMessageEffects: AvailableMessageEffects?,
|
||||
isPremium: Bool
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
@ -808,7 +981,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
reactionItems: reactionItems,
|
||||
availableMessageEffects: availableMessageEffects,
|
||||
isPremium: isPremium
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .none,
|
||||
|
@ -17,6 +17,7 @@ import WallpaperBackgroundNode
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import ReactionButtonListComponent
|
||||
import MultilineTextComponent
|
||||
import ChatInputTextNode
|
||||
|
||||
private final class EffectIcon: Component {
|
||||
enum Content: Equatable {
|
||||
@ -135,7 +136,8 @@ final class MessageItemView: UIView {
|
||||
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
||||
private let backgroundNode: ChatMessageBackground
|
||||
|
||||
private let text = ComponentView<Empty>()
|
||||
private let textClippingContainer: UIView
|
||||
private var textNode: ChatInputTextNode?
|
||||
|
||||
private var effectIcon: ComponentView<Empty>?
|
||||
var effectIconView: UIView? {
|
||||
@ -150,10 +152,15 @@ final class MessageItemView: UIView {
|
||||
self.backgroundNode = ChatMessageBackground()
|
||||
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
||||
|
||||
self.textClippingContainer = UIView()
|
||||
self.textClippingContainer.clipsToBounds = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundWallpaperNode.view)
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
|
||||
self.addSubview(self.textClippingContainer)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
@ -165,9 +172,11 @@ final class MessageItemView: UIView {
|
||||
presentationData: PresentationData,
|
||||
backgroundNode: WallpaperBackgroundNode?,
|
||||
textString: NSAttributedString,
|
||||
sourceTextInputView: ChatInputTextView?,
|
||||
textInsets: UIEdgeInsets,
|
||||
explicitBackgroundSize: CGSize?,
|
||||
maxTextWidth: CGFloat,
|
||||
maxTextHeight: CGFloat,
|
||||
effect: AvailableMessageEffects.MessageEffect?,
|
||||
transition: Transition
|
||||
) -> CGSize {
|
||||
@ -201,33 +210,84 @@ final class MessageItemView: UIView {
|
||||
if let effectIconSize {
|
||||
textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height))
|
||||
}
|
||||
let _ = textCutout
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
placeholderColor: presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper,
|
||||
text: .plain(textString),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.0,
|
||||
cutout: textCutout,
|
||||
insets: UIEdgeInsets()
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 20000.0)
|
||||
let textNode: ChatInputTextNode
|
||||
if let current = self.textNode {
|
||||
textNode = current
|
||||
} else {
|
||||
textNode = ChatInputTextNode(disableTiling: true)
|
||||
textNode.textView.isScrollEnabled = false
|
||||
textNode.isUserInteractionEnabled = false
|
||||
self.textNode = textNode
|
||||
self.textClippingContainer.addSubview(textNode.view)
|
||||
|
||||
if let sourceTextInputView {
|
||||
textNode.textView.defaultTextContainerInset = sourceTextInputView.defaultTextContainerInset
|
||||
}
|
||||
|
||||
let messageAttributedText = NSMutableAttributedString(attributedString: textString)
|
||||
messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length))
|
||||
textNode.attributedText = messageAttributedText
|
||||
}
|
||||
|
||||
let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor
|
||||
let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle
|
||||
if let sourceTextInputView, let textTheme = sourceTextInputView.theme {
|
||||
switch textTheme.quote.lineStyle {
|
||||
case .solid:
|
||||
mappedLineStyle = .solid(color: mainColor)
|
||||
case .doubleDashed:
|
||||
mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear)
|
||||
case .tripleDashed:
|
||||
mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear)
|
||||
}
|
||||
} else {
|
||||
mappedLineStyle = .solid(color: mainColor)
|
||||
}
|
||||
|
||||
textNode.textView.theme = ChatInputTextView.Theme(
|
||||
quote: ChatInputTextView.Theme.Quote(
|
||||
background: mainColor.withMultipliedAlpha(0.1),
|
||||
foreground: mainColor,
|
||||
lineStyle: mappedLineStyle,
|
||||
codeBackground: mainColor.withMultipliedAlpha(0.1),
|
||||
codeForeground: mainColor
|
||||
)
|
||||
)
|
||||
|
||||
let size = CGSize(width: textSize.width + textInsets.left + textInsets.right, height: textSize.height + textInsets.top + textInsets.bottom)
|
||||
let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.frame = textFrame
|
||||
var currentRightInset: CGFloat = 0.0
|
||||
if let sourceTextInputView {
|
||||
currentRightInset = sourceTextInputView.currentRightInset
|
||||
}
|
||||
let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset)
|
||||
textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight))
|
||||
|
||||
let textBoundingRect = textNode.textView.currentTextBoundingRect().integral
|
||||
let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral
|
||||
|
||||
let textWidth = textBoundingRect.width
|
||||
let textSize = CGSize(width: textWidth, height: textHeight)
|
||||
|
||||
var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom)
|
||||
|
||||
let effectInset: CGFloat = 12.0
|
||||
if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset {
|
||||
if lastLineBoundingRect != textBoundingRect {
|
||||
positionedTextSize.height += 11.0
|
||||
} else {
|
||||
positionedTextSize.width += effectInset
|
||||
}
|
||||
}
|
||||
let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom)
|
||||
|
||||
positionedTextSize.height = min(positionedTextSize.height, maxTextHeight)
|
||||
|
||||
let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize)
|
||||
|
||||
let chatTheme: ChatPresentationThemeData
|
||||
if let current = self.chatTheme, current.theme === presentationData.theme {
|
||||
@ -260,6 +320,21 @@ final class MessageItemView: UIView {
|
||||
let previousSize = self.currentSize
|
||||
self.currentSize = backgroundSize
|
||||
|
||||
let textClippingContainerFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: backgroundSize.width - 1.0 - 7.0, height: backgroundSize.height - 1.0 - 1.0))
|
||||
|
||||
var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size)
|
||||
if explicitBackgroundSize != nil, let sourceTextInputView {
|
||||
textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y
|
||||
} else {
|
||||
textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0
|
||||
textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y)
|
||||
}
|
||||
|
||||
transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center)
|
||||
transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds)
|
||||
|
||||
textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))
|
||||
|
||||
if let effectIcon = self.effectIcon, let effectIconSize {
|
||||
if let effectIconView = effectIcon.view {
|
||||
var animateIn = false
|
||||
|
134
submodules/ChatSendMessageActionUI/Sources/SendButton.swift
Normal file
134
submodules/ChatSendMessageActionUI/Sources/SendButton.swift
Normal file
@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import ReactionSelectionNode
|
||||
import ViewControllerComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import ChatMessageBackground
|
||||
import WallpaperBackgroundNode
|
||||
import AppBundle
|
||||
import ActivityIndicator
|
||||
|
||||
final class SendButton: HighlightTrackingButton {
|
||||
private let containerView: UIView
|
||||
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let iconView: UIImageView
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.containerView = UIView()
|
||||
self.containerView.isUserInteractionEnabled = false
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
|
||||
self.iconView = UIImageView()
|
||||
self.iconView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.containerView.clipsToBounds = true
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
self.containerView.layer.addSublayer(self.backgroundLayer)
|
||||
self.containerView.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(
|
||||
context: AccountContext,
|
||||
presentationData: PresentationData,
|
||||
backgroundNode: WallpaperBackgroundNode?,
|
||||
isLoadingEffectAnimation: Bool,
|
||||
size: CGSize,
|
||||
transition: Transition
|
||||
) {
|
||||
let innerSize = CGSize(width: 33.0, height: 33.0)
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: floor((size.width - innerSize.width) * 0.5), y: floor((size.height - innerSize.height) * 0.5)), size: innerSize))
|
||||
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: innerSize.height * 0.5)
|
||||
|
||||
if self.window != nil {
|
||||
if self.backgroundContent == nil, let backgroundNode = backgroundNode as? WallpaperBackgroundNodeImpl {
|
||||
if let backgroundContent = backgroundNode.makeLegacyBubbleBackground(for: .outgoing) {
|
||||
self.backgroundContent = backgroundContent
|
||||
self.containerView.insertSubview(backgroundContent.view, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundContent = self.backgroundContent {
|
||||
transition.setFrame(view: backgroundContent.view, frame: CGRect(origin: CGPoint(), size: innerSize))
|
||||
}
|
||||
|
||||
if [.day, .night].contains(presentationData.theme.referenceTheme.baseTheme) && !presentationData.theme.chat.message.outgoing.bubble.withWallpaper.hasSingleFillColor {
|
||||
self.backgroundContent?.isHidden = false
|
||||
self.backgroundLayer.isHidden = true
|
||||
} else {
|
||||
self.backgroundContent?.isHidden = true
|
||||
self.backgroundLayer.isHidden = false
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = presentationData.theme.list.itemAccentColor.cgColor
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: innerSize))
|
||||
|
||||
if self.iconView.image == nil {
|
||||
self.iconView.image = PresentationResourcesChat.chatInputPanelSendIconImage(presentationData.theme)
|
||||
}
|
||||
|
||||
if let icon = self.iconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((innerSize.width - icon.size.width) * 0.5), y: floor((innerSize.height - icon.size.height) * 0.5)), size: icon.size)
|
||||
transition.setPosition(view: self.iconView, position: iconFrame.center)
|
||||
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
transition.setAlpha(view: self.iconView, alpha: isLoadingEffectAnimation ? 0.0 : 1.0)
|
||||
transition.setScale(view: self.iconView, scale: isLoadingEffectAnimation ? 0.001 : 1.0)
|
||||
}
|
||||
|
||||
if isLoadingEffectAnimation {
|
||||
var animateIn = false
|
||||
let activityIndicator: ActivityIndicator
|
||||
if let current = self.activityIndicator {
|
||||
activityIndicator = current
|
||||
} else {
|
||||
animateIn = true
|
||||
activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.itemCheckColors.foregroundColor, 18.0, 2.0, true))
|
||||
self.activityIndicator = activityIndicator
|
||||
self.containerView.addSubview(activityIndicator.view)
|
||||
}
|
||||
|
||||
let activityIndicatorSize = CGSize(width: 18.0, height: 18.0)
|
||||
let activityIndicatorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - activityIndicatorSize.width) * 0.5), y: floor((innerSize.height - activityIndicatorSize.height) * 0.5) + UIScreenPixel), size: activityIndicatorSize)
|
||||
if animateIn {
|
||||
activityIndicator.view.frame = activityIndicatorFrame
|
||||
transition.animateAlpha(view: activityIndicator.view, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: activityIndicator.view, from: 0.001, to: 1.0)
|
||||
} else {
|
||||
transition.setFrame(view: activityIndicator.view, frame: activityIndicatorFrame)
|
||||
}
|
||||
} else {
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
self.activityIndicator = nil
|
||||
transition.setAlpha(view: activityIndicator.view, alpha: 0.0, completion: { [weak activityIndicator] _ in
|
||||
activityIndicator?.view.removeFromSuperview()
|
||||
})
|
||||
transition.setScale(view: activityIndicator.view, scale: 0.001)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateGlobalRect(rect: CGRect, within containerSize: CGSize, transition: Transition) {
|
||||
if let backgroundContent = self.backgroundContent {
|
||||
backgroundContent.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.containerView.frame.minX, y: rect.minY + self.containerView.frame.minY), size: backgroundContent.bounds.size), within: containerSize, transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,4 +22,10 @@ public class PortalView {
|
||||
portalSuperlayer.insertSublayer(self.view.layer, at: UInt32(index))
|
||||
}
|
||||
}
|
||||
|
||||
public func reloadPortal() {
|
||||
if let sourceView = self.sourceView as? PortalSourceView {
|
||||
self.reloadPortal(sourceView: sourceView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView {
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||
selectedItems: Set(),
|
||||
title: nil,
|
||||
reactionsLocked: false,
|
||||
|
@ -41,6 +41,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private(set) var vibrancyEffectView: UIVisualEffectView?
|
||||
let vibrantExpandedContentContainer: UIView
|
||||
|
||||
private let maskLayer: SimpleLayer
|
||||
private let backgroundClippingLayer: SimpleLayer
|
||||
@ -84,6 +85,8 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
||||
self.smallCircleLayer.cornerCurve = .circular
|
||||
}
|
||||
|
||||
self.vibrantExpandedContentContainer = UIView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.layer.addSublayer(self.backgroundShadowLayer)
|
||||
@ -146,6 +149,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
self.vibrancyEffectView = vibrancyEffectView
|
||||
vibrancyEffectView.contentView.addSubview(self.vibrantExpandedContentContainer)
|
||||
self.backgroundView.addSubview(vibrancyEffectView)
|
||||
}
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ public enum ReactionContextItem: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .reaction(lhsReaction):
|
||||
if case let .reaction(rhsReaction) = rhs {
|
||||
return lhsReaction.reaction == rhsReaction.reaction
|
||||
case let .reaction(lhsReaction, lhsIcon):
|
||||
if case let .reaction(rhsReaction, rhsIcon) = rhs {
|
||||
return lhsReaction.reaction == rhsReaction.reaction && lhsIcon == rhsIcon
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@ -98,11 +98,11 @@ public enum ReactionContextItem: Equatable {
|
||||
}
|
||||
|
||||
case staticEmoji(String)
|
||||
case reaction(ReactionItem)
|
||||
case reaction(item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon)
|
||||
case premium
|
||||
|
||||
public var reaction: ReactionItem.Reaction? {
|
||||
if case let .reaction(item) = self {
|
||||
if case let .reaction(item, _) = self {
|
||||
return item.reaction
|
||||
} else {
|
||||
return nil
|
||||
@ -386,6 +386,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
public var forceDark: Bool = false
|
||||
public var hideBackground: Bool = false
|
||||
|
||||
public var isMessageEffects: Bool = false
|
||||
|
||||
private var didAnimateIn: Bool = false
|
||||
public private(set) var isAnimatingOut: Bool = false
|
||||
public private(set) var isAnimatingOutToReaction: Bool = false
|
||||
@ -829,6 +831,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
public func wantsDisplayBelowKeyboard() -> Bool {
|
||||
if let emojiView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
|
||||
return emojiView.wantsDisplayBelowKeyboard()
|
||||
} else if let stickersView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("stickers"))) as? EmojiPagerContentComponent.View {
|
||||
return stickersView.wantsDisplayBelowKeyboard()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@ -1047,8 +1051,16 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
itemTransition = .immediate
|
||||
|
||||
switch self.items[i] {
|
||||
case let .reaction(item):
|
||||
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: self.reactionsLocked)
|
||||
case let .reaction(item, icon):
|
||||
var isLocked = self.reactionsLocked
|
||||
switch icon {
|
||||
case .locked:
|
||||
isLocked = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, icon: icon, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: isLocked)
|
||||
maskNode = nil
|
||||
case let .staticEmoji(emoji):
|
||||
itemNode = EmojiItemNode(theme: self.presentationData.theme, emoji: emoji)
|
||||
@ -1498,6 +1510,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
animationOffsetY += 54.0
|
||||
} else if self.alwaysAllowPremiumReactions {
|
||||
animationOffsetY += 4.0
|
||||
} else if self.isMessageEffects {
|
||||
animationOffsetY += 54.0
|
||||
transition.animatePositionAdditive(layer: self.backgroundNode.vibrantExpandedContentContainer.layer, offset: CGPoint(x: 0.0, y: -animationOffsetY + floorToScreenPixels(self.animateFromExtensionDistance / 2.0)))
|
||||
} else {
|
||||
animationOffsetY += 46.0 + 54.0 - 4.0
|
||||
}
|
||||
@ -1814,115 +1829,147 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then(
|
||||
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
|
||||
($0, true)
|
||||
}
|
||||
)
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
if isEmojiOnly {
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
|
||||
for emojiString in list {
|
||||
if allEmoticons[emojiString] != nil {
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: nil,
|
||||
content: .staticEmoji(emojiString),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError>
|
||||
if self.isMessageEffects {
|
||||
resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
))
|
||||
return .single(resultGroups)
|
||||
} else {
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
||||
context.engine.stickers.availableReactions() |> take(1),
|
||||
hasPremium |> take(1),
|
||||
remotePacksSignal
|
||||
)
|
||||
|> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
return context.availableMessageEffects
|
||||
|> take(1)
|
||||
|> mapToSignal { availableMessageEffects -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
guard let availableMessageEffects else {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
var filteredEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
for messageEffect in availableMessageEffects.messageEffects {
|
||||
if allEmoticons[messageEffect.emoticon] != nil {
|
||||
filteredEffects.append(messageEffect)
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, alt, _):
|
||||
if !item.file.isPremiumEmoji || hasPremium {
|
||||
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||
result.append((alt, item.file, keyword))
|
||||
} else if alt == query {
|
||||
result.append((alt, item.file, alt))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
var reactionEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
var stickerEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
for messageEffect in filteredEffects {
|
||||
if messageEffect.effectAnimation != nil {
|
||||
reactionEffects.append(messageEffect)
|
||||
} else {
|
||||
stickerEffects.append(messageEffect)
|
||||
}
|
||||
}
|
||||
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
var title: String?
|
||||
var subtitle: String?
|
||||
var actionButtonTitle: String?
|
||||
var isPremiumLocked: Bool
|
||||
var isFeatured: Bool
|
||||
var displayPremiumBadges: Bool
|
||||
var hasEdit: Bool
|
||||
var headerItem: EntityKeyboardAnimationData?
|
||||
var items: [EmojiPagerContentComponent.Item]
|
||||
}
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
var resultGroups: [ItemGroup] = []
|
||||
var resultGroupIndexById: [AnyHashable: Int] = [:]
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let groupId = i == 0 ? "reactions" : "stickers"
|
||||
for item in i == 0 ? reactionEffects : stickerEffects {
|
||||
let itemFile: TelegramMediaFile = item.effectSticker
|
||||
|
||||
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
||||
if itemFile.isCustomTemplateEmoji {
|
||||
tintMode = .primary
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
tintMode: tintMode
|
||||
)
|
||||
items.append(item)
|
||||
|
||||
if let groupIndex = resultGroupIndexById[groupId] {
|
||||
resultGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
resultGroupIndexById[groupId] = resultGroups.count
|
||||
//TODO:localize
|
||||
resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
||||
let hasClear = false
|
||||
let isEmbedded = false
|
||||
|
||||
return EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: group.supergroupId,
|
||||
groupId: group.id,
|
||||
title: group.title,
|
||||
subtitle: group.subtitle,
|
||||
badge: nil,
|
||||
actionButtonTitle: group.actionButtonTitle,
|
||||
isFeatured: group.isFeatured,
|
||||
isPremiumLocked: group.isPremiumLocked,
|
||||
isEmbedded: isEmbedded,
|
||||
hasClear: hasClear,
|
||||
hasEdit: group.hasEdit,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: group.displayPremiumBadges,
|
||||
headerItem: group.headerItem,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: group.items
|
||||
)
|
||||
}
|
||||
|
||||
return .single(allItemGroups)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then(
|
||||
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
|
||||
($0, true)
|
||||
}
|
||||
)
|
||||
|
||||
resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
if isEmojiOnly {
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
|
||||
for emojiString in list {
|
||||
if allEmoticons[emojiString] != nil {
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: nil,
|
||||
content: .staticEmoji(emojiString),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
@ -1942,59 +1989,138 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
))
|
||||
|
||||
for (collectionId, info, _, _) in foundPacks.sets.infos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
var topItems: [StickerPackItem] = []
|
||||
for e in foundPacks.sets.entries {
|
||||
if let item = e.item as? StickerPackItem {
|
||||
if e.index.collectionId == collectionId {
|
||||
topItems.append(item)
|
||||
return .single(resultGroups)
|
||||
} else {
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
||||
context.engine.stickers.availableReactions() |> take(1),
|
||||
hasPremium |> take(1),
|
||||
remotePacksSignal
|
||||
)
|
||||
|> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, alt, _):
|
||||
if !item.file.isPremiumEmoji || hasPremium {
|
||||
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||
result.append((alt, item.file, keyword))
|
||||
} else if alt == query {
|
||||
result.append((alt, item.file, alt))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var groupItems: [EmojiPagerContentComponent.Item] = []
|
||||
for item in topItems {
|
||||
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
||||
if item.file.isCustomTemplateEmoji {
|
||||
tintMode = .primary
|
||||
}
|
||||
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: tintMode
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
|
||||
groupItems.append(resultItem)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: AnyHashable(info.id),
|
||||
groupId: AnyHashable(info.id),
|
||||
title: info.title,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: 3,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: groupItems
|
||||
))
|
||||
}
|
||||
|
||||
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
))
|
||||
|
||||
for (collectionId, info, _, _) in foundPacks.sets.infos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
var topItems: [StickerPackItem] = []
|
||||
for e in foundPacks.sets.entries {
|
||||
if let item = e.item as? StickerPackItem {
|
||||
if e.index.collectionId == collectionId {
|
||||
topItems.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupItems: [EmojiPagerContentComponent.Item] = []
|
||||
for item in topItems {
|
||||
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
||||
if item.file.isCustomTemplateEmoji {
|
||||
tintMode = .primary
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: tintMode
|
||||
)
|
||||
|
||||
groupItems.append(resultItem)
|
||||
}
|
||||
|
||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: AnyHashable(info.id),
|
||||
groupId: AnyHashable(info.id),
|
||||
title: info.title,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: 3,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: groupItems
|
||||
))
|
||||
}
|
||||
}
|
||||
return resultGroups
|
||||
}
|
||||
return resultGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2013,45 +2139,156 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}))
|
||||
}
|
||||
case let .category(value):
|
||||
let resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|
||||
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
let context = self.context
|
||||
let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError>
|
||||
if self.isMessageEffects {
|
||||
let keywords: Signal<[String], NoError> = .single(value.identifiers)
|
||||
resultSignal = keywords
|
||||
|> mapToSignal { keywords -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
allEmoticons[keyword] = keyword
|
||||
}
|
||||
|
||||
return context.availableMessageEffects
|
||||
|> take(1)
|
||||
|> mapToSignal { availableMessageEffects -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
guard let availableMessageEffects else {
|
||||
return .single(([], true))
|
||||
}
|
||||
|
||||
var filteredEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
for messageEffect in availableMessageEffects.messageEffects {
|
||||
if allEmoticons[messageEffect.emoticon] != nil {
|
||||
filteredEffects.append(messageEffect)
|
||||
}
|
||||
}
|
||||
|
||||
var reactionEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
var stickerEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||
for messageEffect in filteredEffects {
|
||||
if messageEffect.effectAnimation != nil {
|
||||
reactionEffects.append(messageEffect)
|
||||
} else {
|
||||
stickerEffects.append(messageEffect)
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
var title: String?
|
||||
var subtitle: String?
|
||||
var actionButtonTitle: String?
|
||||
var isPremiumLocked: Bool
|
||||
var isFeatured: Bool
|
||||
var displayPremiumBadges: Bool
|
||||
var hasEdit: Bool
|
||||
var headerItem: EntityKeyboardAnimationData?
|
||||
var items: [EmojiPagerContentComponent.Item]
|
||||
}
|
||||
|
||||
var resultGroups: [ItemGroup] = []
|
||||
var resultGroupIndexById: [AnyHashable: Int] = [:]
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let groupId = i == 0 ? "reactions" : "stickers"
|
||||
for item in i == 0 ? reactionEffects : stickerEffects {
|
||||
let itemFile: TelegramMediaFile = item.effectSticker
|
||||
|
||||
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
||||
if itemFile.isCustomTemplateEmoji {
|
||||
tintMode = .primary
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: tintMode
|
||||
)
|
||||
|
||||
if let groupIndex = resultGroupIndexById[groupId] {
|
||||
resultGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
resultGroupIndexById[groupId] = resultGroups.count
|
||||
//TODO:localize
|
||||
resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
||||
let hasClear = false
|
||||
let isEmbedded = false
|
||||
|
||||
return EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: group.supergroupId,
|
||||
groupId: group.id,
|
||||
title: group.title,
|
||||
subtitle: group.subtitle,
|
||||
badge: nil,
|
||||
actionButtonTitle: group.actionButtonTitle,
|
||||
isFeatured: group.isFeatured,
|
||||
isPremiumLocked: group.isPremiumLocked,
|
||||
isEmbedded: isEmbedded,
|
||||
hasClear: hasClear,
|
||||
hasEdit: group.hasEdit,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: group.displayPremiumBadges,
|
||||
headerItem: group.headerItem,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: group.items
|
||||
)
|
||||
}
|
||||
|
||||
return .single((allItemGroups, true))
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)], isFinalResult))
|
||||
} else {
|
||||
resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|
||||
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)], isFinalResult))
|
||||
}
|
||||
}
|
||||
|
||||
var version = 0
|
||||
@ -2101,7 +2338,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
peekBehavior: nil,
|
||||
customLayout: emojiContentLayout,
|
||||
externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground(
|
||||
effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView
|
||||
effectContainerView: self.backgroundNode.vibrantExpandedContentContainer
|
||||
),
|
||||
externalExpansionView: self.view,
|
||||
customContentView: nil,
|
||||
@ -2310,7 +2547,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
|
||||
if let customReactionSource = self.customReactionSource {
|
||||
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
|
||||
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, icon: .none, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
|
||||
if let contents = customReactionSource.layer.contents {
|
||||
itemNode.setCustomContents(contents: contents)
|
||||
}
|
||||
@ -2737,11 +2974,13 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring))
|
||||
} else if let reaction = self.reaction(at: point) {
|
||||
switch reaction {
|
||||
case let .reaction(reactionItem):
|
||||
case let .reaction(reactionItem, icon):
|
||||
if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable {
|
||||
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||
} else if self.reactionsLocked {
|
||||
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||
} else if case .locked = icon {
|
||||
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||
} else {
|
||||
self.reactionSelected?(reactionItem.updateMessageReaction, false)
|
||||
}
|
||||
@ -2915,7 +3154,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
if !itemNode.isAnimationLoaded {
|
||||
return nil
|
||||
}
|
||||
return .reaction(itemNode.item)
|
||||
return .reaction(item: itemNode.item, icon: itemNode.icon)
|
||||
} else if let itemNode = itemNode as? EmojiItemNode {
|
||||
return .staticEmoji(itemNode.emoji)
|
||||
} else if let _ = itemNode as? PremiumReactionsNode {
|
||||
@ -2999,7 +3238,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
itemNode = currentItemNode
|
||||
} else {
|
||||
let animationRenderer = MultiAnimationRendererImpl()
|
||||
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
|
||||
itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
|
||||
}
|
||||
self.itemNode = itemNode
|
||||
} else {
|
||||
|
@ -14,6 +14,7 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ShimmerEffect
|
||||
import GenerateStickerPlaceholderImage
|
||||
import EntityKeyboard
|
||||
|
||||
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
||||
@ -52,13 +53,14 @@ protocol ReactionItemNode: ASDisplayNode {
|
||||
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
|
||||
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 12.0, color: .white)!.withRenderingMode(.alwaysTemplate)
|
||||
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate)
|
||||
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
|
||||
|
||||
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let item: ReactionItem
|
||||
let icon: EmojiPagerContentComponent.Item.Icon
|
||||
private let loopIdle: Bool
|
||||
private let isLocked: Bool
|
||||
private let hasAppearAnimation: Bool
|
||||
@ -102,10 +104,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
return self.staticAnimationNode.currentFrameImage != nil
|
||||
}
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.item = item
|
||||
self.icon = icon
|
||||
self.loopIdle = loopIdle
|
||||
self.isLocked = isLocked
|
||||
self.hasAppearAnimation = hasAppearAnimation
|
||||
@ -475,11 +478,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
}
|
||||
|
||||
if let lockBackgroundView = self.lockBackgroundView, let lockIconView = self.lockIconView, let iconImage = lockIconView.image {
|
||||
let lockSize: CGFloat = 12.0
|
||||
let lockSize: CGFloat = 16.0
|
||||
let iconBackgroundFrame = CGRect(origin: CGPoint(x: animationFrame.maxX - lockSize, y: animationFrame.maxY - lockSize), size: CGSize(width: lockSize, height: lockSize))
|
||||
transition.updateFrame(view: lockBackgroundView, frame: iconBackgroundFrame)
|
||||
|
||||
let iconFactor: CGFloat = 0.7
|
||||
let iconFactor: CGFloat = 1.0
|
||||
let iconImageSize = CGSize(width: floor(iconImage.size.width * iconFactor), height: floor(iconImage.size.height * iconFactor))
|
||||
|
||||
transition.updateFrame(view: lockIconView, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floorToScreenPixels((iconBackgroundFrame.width - iconImageSize.width) * 0.5), y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - iconImageSize.height) * 0.5)), size: iconImageSize))
|
||||
|
@ -156,7 +156,7 @@ private extension AvailableMessageEffects.MessageEffect {
|
||||
return nil
|
||||
}
|
||||
|
||||
let isPremium = (flags & (1 << 3)) != 0
|
||||
let isPremium = (flags & (1 << 2)) != 0
|
||||
self.init(
|
||||
id: id,
|
||||
isPremium: isPremium,
|
||||
@ -239,7 +239,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
|
||||
break
|
||||
}
|
||||
|
||||
var signals: [Signal<Never, NoError>] = []
|
||||
/*var signals: [Signal<Never, NoError>] = []
|
||||
|
||||
if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) {
|
||||
var resources: [MediaResource] = []
|
||||
@ -271,7 +271,9 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
|
||||
}
|
||||
|
||||
return combineLatest(signals)
|
||||
|> ignoreValues
|
||||
|> ignoreValues*/
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|> switchToLatest
|
||||
})
|
||||
|
@ -312,6 +312,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
public var currentRightInset: CGFloat {
|
||||
return self.customTextContainer.rightInset
|
||||
}
|
||||
|
||||
private var didInitializePrimaryInputLanguage: Bool = false
|
||||
public var initialPrimaryLanguage: String?
|
||||
|
||||
@ -656,6 +660,45 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
||||
self.isUpdatingLayout = false
|
||||
}
|
||||
|
||||
public func currentTextBoundingRect() -> CGRect {
|
||||
let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil)
|
||||
|
||||
var boundingRect = CGRect()
|
||||
var startIndex = glyphRange.lowerBound
|
||||
while startIndex < glyphRange.upperBound {
|
||||
var effectiveRange = NSRange(location: NSNotFound, length: 0)
|
||||
let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange)
|
||||
if boundingRect.isEmpty {
|
||||
boundingRect = rect
|
||||
} else {
|
||||
boundingRect = boundingRect.union(rect)
|
||||
}
|
||||
if effectiveRange.location != NSNotFound {
|
||||
startIndex = max(startIndex + 1, effectiveRange.upperBound)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return boundingRect
|
||||
}
|
||||
|
||||
public func lastLineBoundingRect() -> CGRect {
|
||||
let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil)
|
||||
var boundingRect = CGRect()
|
||||
var startIndex = glyphRange.lowerBound
|
||||
while startIndex < glyphRange.upperBound {
|
||||
var effectiveRange = NSRange(location: NSNotFound, length: 0)
|
||||
let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange)
|
||||
boundingRect = rect
|
||||
if effectiveRange.location != NSNotFound {
|
||||
startIndex = max(startIndex + 1, effectiveRange.upperBound)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return boundingRect
|
||||
}
|
||||
|
||||
public func updateTextElements() {
|
||||
var blockQuoteIndex = 0
|
||||
var validBlockQuotes: [Int] = []
|
||||
|
@ -633,6 +633,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
|
||||
self.visibilityStatus = self.visibility != .none
|
||||
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -648,8 +650,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
containerSize: credibilityIconView.bounds.size
|
||||
)
|
||||
}
|
||||
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5840,7 +5840,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
do {
|
||||
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
|
||||
|
||||
let additionalAnimationNode = LottieMetalAnimatedStickerNode()
|
||||
let additionalAnimationNode: AnimatedStickerNode
|
||||
#if targetEnvironment(simulator)
|
||||
additionalAnimationNode = DirectAnimatedStickerNode()
|
||||
#else
|
||||
additionalAnimationNode = LottieMetalAnimatedStickerNode()
|
||||
#endif
|
||||
additionalAnimationNode.updateLayout(size: animationSize)
|
||||
additionalAnimationNode.setup(source: source, width: Int(animationSize.width), height: Int(animationSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
||||
var animationFrame: CGRect
|
||||
@ -5925,32 +5930,46 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
return
|
||||
}
|
||||
|
||||
let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations
|
||||
var isPlaying = true
|
||||
if case let .visible(_, subRect) = self.visibility {
|
||||
if subRect.minY > 32.0 {
|
||||
isPlaying = false
|
||||
}
|
||||
} else {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
if self.forceStopAnimations {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
if !isPlaying {
|
||||
self.removeAdditionalAnimations()
|
||||
}
|
||||
|
||||
var alreadySeen = true
|
||||
if item.message.flags.contains(.Incoming) {
|
||||
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
|
||||
if unreadRange.contains(item.message.id.id) {
|
||||
if isPlaying {
|
||||
var alreadySeen = true
|
||||
if item.message.flags.contains(.Incoming) {
|
||||
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
|
||||
if unreadRange.contains(item.message.id.id) {
|
||||
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
alreadySeen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.didChangeFromPendingToSent {
|
||||
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
alreadySeen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.didChangeFromPendingToSent {
|
||||
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
alreadySeen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadySeen {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
|
||||
self.playMessageEffect(force: false)
|
||||
if !alreadySeen {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
|
||||
self.playMessageEffect(force: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||
selectedItems: actions.editTags,
|
||||
title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag,
|
||||
reactionsLocked: false,
|
||||
|
@ -31,7 +31,7 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||
selectedItems: Set(),
|
||||
title: isSingleMessage ? presentationData.strings.Chat_ForwardToSavedMessageTagSelectionTitle : presentationData.strings.Chat_ForwardToSavedMessagesTagSelectionTitle,
|
||||
reactionsLocked: false,
|
||||
|
@ -47,6 +47,7 @@ swift_library(
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -602,14 +602,23 @@ private class PassthroughShapeLayer: CAShapeLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private let itemBadgeTextFont: UIFont = {
|
||||
return Font.regular(10.0)
|
||||
}()
|
||||
|
||||
private final class PremiumBadgeView: UIView {
|
||||
private let context: AccountContext
|
||||
|
||||
private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
|
||||
|
||||
let contentLayer: SimpleLayer
|
||||
private let overlayColorLayer: SimpleLayer
|
||||
private let iconLayer: SimpleLayer
|
||||
private var customFileLayer: InlineFileIconLayer?
|
||||
|
||||
init() {
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
self.contentLayer = SimpleLayer()
|
||||
self.contentLayer.contentsGravity = .resize
|
||||
self.contentLayer.masksToBounds = true
|
||||
@ -641,6 +650,47 @@ private final class PremiumBadgeView: UIView {
|
||||
self.iconLayer.contents = featuredBadgeIcon?.cgImage
|
||||
case .locked:
|
||||
self.iconLayer.contents = lockedBadgeIcon?.cgImage
|
||||
case let .text(text):
|
||||
let string = NSAttributedString(string: text, font: itemBadgeTextFont)
|
||||
let size = CGSize(width: 12.0, height: 12.0)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5)))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.iconLayer.contents = image?.cgImage
|
||||
case .customFile:
|
||||
self.iconLayer.contents = nil
|
||||
}
|
||||
|
||||
if case let .customFile(customFile) = badge {
|
||||
let customFileLayer: InlineFileIconLayer
|
||||
if let current = self.customFileLayer {
|
||||
customFileLayer = current
|
||||
} else {
|
||||
customFileLayer = InlineFileIconLayer(
|
||||
context: self.context,
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: false,
|
||||
file: customFile,
|
||||
cache: self.context.animationCache,
|
||||
renderer: self.context.animationRenderer,
|
||||
unique: false,
|
||||
placeholderColor: .clear,
|
||||
pointSize: CGSize(width: 18.0, height: 18.0),
|
||||
dynamicColor: nil
|
||||
)
|
||||
self.customFileLayer = customFileLayer
|
||||
self.layer.addSublayer(customFileLayer)
|
||||
}
|
||||
let _ = customFileLayer
|
||||
} else {
|
||||
if let customFileLayer = self.customFileLayer {
|
||||
self.customFileLayer = nil
|
||||
customFileLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,6 +702,17 @@ private final class PremiumBadgeView: UIView {
|
||||
iconInset = 0.0
|
||||
case .locked:
|
||||
iconInset = 0.0
|
||||
case .text, .customFile:
|
||||
iconInset = 0.0
|
||||
}
|
||||
|
||||
switch badge {
|
||||
case .text, .customFile:
|
||||
self.contentLayer.isHidden = true
|
||||
self.overlayColorLayer.isHidden = true
|
||||
default:
|
||||
self.contentLayer.isHidden = false
|
||||
self.overlayColorLayer.isHidden = false
|
||||
}
|
||||
|
||||
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
|
||||
@ -663,6 +724,11 @@ private final class PremiumBadgeView: UIView {
|
||||
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
|
||||
|
||||
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
|
||||
|
||||
if let customFileLayer = self.customFileLayer {
|
||||
let iconSize = CGSize(width: 18.0, height: 18.0)
|
||||
transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2608,6 +2674,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
case none
|
||||
case locked
|
||||
case premium
|
||||
case text(String)
|
||||
case customFile(TelegramMediaFile)
|
||||
}
|
||||
|
||||
public enum TintMode: Equatable {
|
||||
@ -3448,13 +3516,16 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
enum Badge {
|
||||
enum Badge: Equatable {
|
||||
case premium
|
||||
case locked
|
||||
case featured
|
||||
case text(String)
|
||||
case customFile(TelegramMediaFile)
|
||||
}
|
||||
|
||||
public let item: Item
|
||||
private let context: AccountContext
|
||||
|
||||
private var content: ItemContent
|
||||
private var theme: PresentationTheme?
|
||||
@ -3566,6 +3637,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
||||
) {
|
||||
self.item = item
|
||||
self.context = context
|
||||
self.content = content
|
||||
self.placeholderColor = placeholderColor
|
||||
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||
@ -3717,6 +3789,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
self.context = layer.context
|
||||
self.item = layer.item
|
||||
|
||||
self.content = layer.content
|
||||
@ -3837,7 +3910,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
premiumBadgeView = current
|
||||
} else {
|
||||
badgeTransition = .immediate
|
||||
premiumBadgeView = PremiumBadgeView()
|
||||
premiumBadgeView = PremiumBadgeView(context: self.context)
|
||||
self.premiumBadgeView = premiumBadgeView
|
||||
self.addSublayer(premiumBadgeView.layer)
|
||||
}
|
||||
@ -6202,6 +6275,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
badge = .locked
|
||||
case .premium:
|
||||
badge = .premium
|
||||
case let .text(value):
|
||||
badge = .text(value)
|
||||
case let .customFile(customFile):
|
||||
badge = .customFile(customFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2176,7 +2176,7 @@ public extension EmojiPagerContentComponent {
|
||||
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = .single(nil)
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
|
||||
|
||||
return combineLatest(
|
||||
hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false),
|
||||
@ -2225,13 +2225,30 @@ public extension EmojiPagerContentComponent {
|
||||
tintMode = .primary
|
||||
}
|
||||
|
||||
let icon: EmojiPagerContentComponent.Item.Icon
|
||||
if i == 0 {
|
||||
if !hasPremium && item.isPremium {
|
||||
icon = .locked
|
||||
} else {
|
||||
icon = .none
|
||||
}
|
||||
} else {
|
||||
if !hasPremium && item.isPremium {
|
||||
icon = .locked
|
||||
} else if let staticIcon = item.staticIcon {
|
||||
icon = .customFile(staticIcon)
|
||||
} else {
|
||||
icon = .text(item.emoticon)
|
||||
}
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
icon: icon,
|
||||
tintMode: tintMode
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,375 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import MultiAnimationRenderer
|
||||
import AnimationCache
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
import GenerateStickerPlaceholderImage
|
||||
import EmojiTextAttachmentView
|
||||
import LottieAnimationCache
|
||||
|
||||
public final class InlineFileIconLayer: MultiAnimationRenderTarget {
|
||||
private final class Arguments {
|
||||
let context: InlineFileIconLayer.Context
|
||||
let userLocation: MediaResourceUserLocation
|
||||
let file: TelegramMediaFile
|
||||
let cache: AnimationCache
|
||||
let renderer: MultiAnimationRenderer
|
||||
let unique: Bool
|
||||
let placeholderColor: UIColor
|
||||
|
||||
let pointSize: CGSize
|
||||
let pixelSize: CGSize
|
||||
|
||||
init(context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, pointSize: CGSize, pixelSize: CGSize) {
|
||||
self.context = context
|
||||
self.userLocation = userLocation
|
||||
self.file = file
|
||||
self.cache = cache
|
||||
self.renderer = renderer
|
||||
self.unique = unique
|
||||
self.placeholderColor = placeholderColor
|
||||
self.pointSize = pointSize
|
||||
self.pixelSize = pixelSize
|
||||
}
|
||||
}
|
||||
|
||||
public enum Context: Equatable {
|
||||
public final class Custom: Equatable {
|
||||
public let postbox: Postbox
|
||||
public let energyUsageSettings: () -> EnergyUsageSettings
|
||||
public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>
|
||||
|
||||
public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) {
|
||||
self.postbox = postbox
|
||||
self.energyUsageSettings = energyUsageSettings
|
||||
self.resolveInlineStickers = resolveInlineStickers
|
||||
}
|
||||
|
||||
public static func ==(lhs: Custom, rhs: Custom) -> Bool {
|
||||
if lhs.postbox !== rhs.postbox {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case account(AccountContext)
|
||||
case custom(Custom)
|
||||
|
||||
var postbox: Postbox {
|
||||
switch self {
|
||||
case let .account(account):
|
||||
return account.account.postbox
|
||||
case let .custom(custom):
|
||||
return custom.postbox
|
||||
}
|
||||
}
|
||||
|
||||
var energyUsageSettings: EnergyUsageSettings {
|
||||
switch self {
|
||||
case let .account(account):
|
||||
return account.sharedContext.energyUsageSettings
|
||||
case let .custom(custom):
|
||||
return custom.energyUsageSettings()
|
||||
}
|
||||
}
|
||||
|
||||
func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> {
|
||||
switch self {
|
||||
case let .account(account):
|
||||
return account.engine.stickers.resolveInlineStickers(fileIds: fileIds)
|
||||
case let .custom(custom):
|
||||
return custom.resolveInlineStickers(fileIds)
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: Context, rhs: Context) -> Bool {
|
||||
switch lhs {
|
||||
case let .account(lhsContext):
|
||||
if case let .account(rhsContext) = rhs, lhsContext === rhsContext {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .custom(custom):
|
||||
if case .custom(custom) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static let queue = Queue()
|
||||
|
||||
public struct Key: Hashable {
|
||||
public var id: Int64
|
||||
public var index: Int
|
||||
|
||||
public init(id: Int64, index: Int) {
|
||||
self.id = id
|
||||
self.index = index
|
||||
}
|
||||
}
|
||||
|
||||
private let arguments: Arguments?
|
||||
|
||||
private var isDisplayingPlaceholder: Bool = false
|
||||
private var didProcessTintColor: Bool = false
|
||||
|
||||
public private(set) var file: TelegramMediaFile?
|
||||
private var infoDisposable: Disposable?
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
private var loadDisposable: Disposable?
|
||||
|
||||
private var _contentTintColor: UIColor?
|
||||
public var contentTintColor: UIColor? {
|
||||
get {
|
||||
return self._contentTintColor
|
||||
}
|
||||
set(value) {
|
||||
if self._contentTintColor != value {
|
||||
self._contentTintColor = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _dynamicColor: UIColor?
|
||||
public var dynamicColor: UIColor? {
|
||||
get {
|
||||
return self._dynamicColor
|
||||
}
|
||||
set(value) {
|
||||
if self._dynamicColor != value {
|
||||
self._dynamicColor = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentLoopCount: Int = 0
|
||||
|
||||
private var isInHierarchyValue: Bool = false
|
||||
|
||||
public convenience init(
|
||||
context: AccountContext,
|
||||
userLocation: MediaResourceUserLocation,
|
||||
attemptSynchronousLoad: Bool,
|
||||
file: TelegramMediaFile,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
unique: Bool = false,
|
||||
placeholderColor: UIColor,
|
||||
pointSize: CGSize,
|
||||
dynamicColor: UIColor? = nil
|
||||
) {
|
||||
self.init(
|
||||
context: .account(context),
|
||||
userLocation: userLocation,
|
||||
attemptSynchronousLoad: attemptSynchronousLoad,
|
||||
file: file,
|
||||
cache: cache,
|
||||
renderer: renderer,
|
||||
unique: unique,
|
||||
placeholderColor: placeholderColor,
|
||||
pointSize: pointSize,
|
||||
dynamicColor: dynamicColor
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
context: InlineFileIconLayer.Context,
|
||||
userLocation: MediaResourceUserLocation,
|
||||
attemptSynchronousLoad: Bool,
|
||||
file: TelegramMediaFile,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
unique: Bool = false,
|
||||
placeholderColor: UIColor,
|
||||
pointSize: CGSize,
|
||||
dynamicColor: UIColor? = nil
|
||||
) {
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
|
||||
self.arguments = Arguments(
|
||||
context: context,
|
||||
userLocation: userLocation,
|
||||
file: file,
|
||||
cache: cache,
|
||||
renderer: renderer,
|
||||
unique: unique,
|
||||
placeholderColor: placeholderColor,
|
||||
pointSize: pointSize,
|
||||
pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
|
||||
)
|
||||
|
||||
self._dynamicColor = dynamicColor
|
||||
|
||||
super.init()
|
||||
|
||||
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
self.arguments = nil
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.loadDisposable?.dispose()
|
||||
self.infoDisposable?.dispose()
|
||||
self.disposable?.dispose()
|
||||
self.fetchDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func action(forKey event: String) -> CAAction? {
|
||||
if event == kCAOnOrderIn {
|
||||
self.isInHierarchyValue = true
|
||||
} else if event == kCAOnOrderOut {
|
||||
self.isInHierarchyValue = false
|
||||
}
|
||||
return nullAction
|
||||
}
|
||||
|
||||
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
|
||||
guard let arguments = self.arguments else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.file?.fileId == file.fileId {
|
||||
return
|
||||
}
|
||||
|
||||
self.file = file
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) {
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
self.isDisplayingPlaceholder = true
|
||||
}
|
||||
}
|
||||
|
||||
self.loadAnimation()
|
||||
} else {
|
||||
let isTemplate = file.isCustomTemplateEmoji
|
||||
|
||||
let pointSize = arguments.pointSize
|
||||
let placeholderColor = arguments.placeholderColor
|
||||
let isThumbnailCancelled = Atomic<Bool>(value: false)
|
||||
self.loadDisposable = arguments.renderer.loadFirstFrame(
|
||||
target: self,
|
||||
cache: arguments.cache,
|
||||
itemId: file.resource.id.stringRepresentation,
|
||||
size: arguments.pixelSize,
|
||||
fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in
|
||||
if !result {
|
||||
MultiAnimationRendererImpl.firstFrameQueue.async {
|
||||
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else {
|
||||
return
|
||||
}
|
||||
if let image = image {
|
||||
strongSelf.contents = image.cgImage
|
||||
strongSelf.isDisplayingPlaceholder = true
|
||||
}
|
||||
|
||||
if isFinal {
|
||||
strongSelf.loadAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = isThumbnailCancelled.swap(true)
|
||||
strongSelf.loadAnimation()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAnimation() {
|
||||
/*guard let arguments = self.arguments else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let file = self.file else {
|
||||
return
|
||||
}
|
||||
|
||||
let isTemplate = file.isCustomTemplateEmoji
|
||||
|
||||
let context = arguments.context
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
|
||||
let keyframeOnly = arguments.pixelSize.width >= 120.0
|
||||
|
||||
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
|
||||
} else {
|
||||
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in
|
||||
let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in
|
||||
guard result.complete else {
|
||||
return
|
||||
}
|
||||
|
||||
cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil)
|
||||
})
|
||||
|
||||
let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start()
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
fetchDisposable.dispose()
|
||||
}
|
||||
})
|
||||
}*/
|
||||
}
|
||||
|
||||
override public func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
||||
if self.isDisplayingPlaceholder == displayPlaceholder {
|
||||
return
|
||||
}
|
||||
self.isDisplayingPlaceholder = displayPlaceholder
|
||||
}
|
||||
|
||||
override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
|
||||
if self.isDisplayingPlaceholder {
|
||||
self.isDisplayingPlaceholder = false
|
||||
|
||||
if let current = self.contents {
|
||||
let previousLayer = SimpleLayer()
|
||||
previousLayer.contents = current
|
||||
previousLayer.frame = self.frame
|
||||
self.superlayer?.insertSublayer(previousLayer, below: self)
|
||||
previousLayer.opacity = 0.0
|
||||
previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in
|
||||
previousLayer?.removeFromSuperlayer()
|
||||
})
|
||||
|
||||
self.contents = contents
|
||||
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
} else {
|
||||
self.contents = contents
|
||||
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
} else {
|
||||
self.contents = contents
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ objc_library(
|
||||
copts = [
|
||||
"-Werror",
|
||||
"-I{}/Sources".format(package_name()),
|
||||
"-O2",
|
||||
],
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
@ -32,3 +31,18 @@ objc_library(
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "LottieCppBinding",
|
||||
srcs = [],
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
copts = [],
|
||||
visibility = ["//visibility:public"],
|
||||
linkstatic = 1,
|
||||
tags = ["swift_module=LottieCppBinding"],
|
||||
)
|
||||
|
@ -21,6 +21,30 @@ class RenderTreeNode;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
CGRect bounds;
|
||||
CGPoint position;
|
||||
CATransform3D transform;
|
||||
double opacity;
|
||||
bool masksToBounds;
|
||||
bool isHidden;
|
||||
} LottieRenderNodeLayerData;
|
||||
|
||||
typedef struct {
|
||||
int64_t internalId;
|
||||
bool isValid;
|
||||
LottieRenderNodeLayerData layer;
|
||||
CGRect globalRect;
|
||||
CGRect localRect;
|
||||
CATransform3D globalTransform;
|
||||
bool drawsContent;
|
||||
bool hasSimpleContents;
|
||||
int drawContentDescendants;
|
||||
bool isInvertedMatte;
|
||||
int64_t maskId;
|
||||
int subnodeCount;
|
||||
} LottieRenderNodeProxy;
|
||||
|
||||
@interface LottieAnimationContainer : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation;
|
||||
@ -34,6 +58,10 @@ extern "C" {
|
||||
- (std::shared_ptr<lottie::RenderTreeNode>)internalGetRootRenderTreeNode;
|
||||
#endif
|
||||
|
||||
- (int64_t)getRootRenderNodeProxy;
|
||||
- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct));
|
||||
- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct));
|
||||
|
||||
@end
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
@ -10,19 +10,10 @@ void batchInterpolate(std::vector<PathElement> const &from, std::vector<PathElem
|
||||
elementCount = (int)to.size();
|
||||
}
|
||||
|
||||
if (sizeof(PathElement) == 8 * 2 * 3) {
|
||||
resultPath.setElementCount(elementCount);
|
||||
vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3);
|
||||
} else {
|
||||
for (int i = 0; i < elementCount; i++) {
|
||||
const auto &fromVertex = from[i].vertex;
|
||||
const auto &toVertex = to[i].vertex;
|
||||
|
||||
auto vertex = ValueInterpolator<CurveVertex>::interpolate(fromVertex, toVertex, amount);
|
||||
|
||||
resultPath.updateVertex(vertex, i, false);
|
||||
}
|
||||
}
|
||||
static_assert(sizeof(PathElement) == 8 * 2 * 3);
|
||||
|
||||
resultPath.setElementCount(elementCount);
|
||||
vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -178,15 +178,6 @@ public:
|
||||
}
|
||||
} else {
|
||||
batchInterpolate(value.elements(), to.elements(), resultPath, amount);
|
||||
|
||||
/*for (int i = 0; i < elementCount; i++) {
|
||||
const auto &fromVertex = value.elements()[i].vertex;
|
||||
const auto &toVertex = to.elements()[i].vertex;
|
||||
|
||||
auto vertex = ValueInterpolator<CurveVertex>::interpolate(fromVertex, toVertex, amount);
|
||||
|
||||
resultPath.updateVertex(vertex, i, false);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "LottieAnimationInternal.h"
|
||||
#include "RenderNode.hpp"
|
||||
#include "LottieRenderTreeInternal.h"
|
||||
#include <LottieCpp/VectorsCocoa.h>
|
||||
|
||||
namespace lottie {
|
||||
|
||||
@ -367,6 +368,48 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
|
||||
return renderNode;
|
||||
}
|
||||
|
||||
- (int64_t)getRootRenderNodeProxy {
|
||||
std::shared_ptr<lottie::RenderTreeNode> renderNode = [self internalGetRootRenderTreeNode];
|
||||
return (int64_t)renderNode.get();
|
||||
}
|
||||
|
||||
- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)) {
|
||||
lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId;
|
||||
|
||||
LottieRenderNodeProxy result;
|
||||
|
||||
result.internalId = nodeId;
|
||||
result.isValid = node->renderData.isValid;
|
||||
|
||||
result.layer.bounds = CGRectMake(node->renderData.layer._bounds.x, node->renderData.layer._bounds.y, node->renderData.layer._bounds.width, node->renderData.layer._bounds.height);
|
||||
result.layer.position = CGPointMake(node->renderData.layer._position.x, node->renderData.layer._position.y);
|
||||
result.layer.transform = lottie::nativeTransform(node->renderData.layer._transform);
|
||||
result.layer.opacity = node->renderData.layer._opacity;
|
||||
result.layer.masksToBounds = node->renderData.layer._masksToBounds;
|
||||
result.layer.isHidden = node->renderData.layer._isHidden;
|
||||
|
||||
result.globalRect = CGRectMake(node->renderData.globalRect.x, node->renderData.globalRect.y, node->renderData.globalRect.width, node->renderData.globalRect.height);
|
||||
result.localRect = CGRectMake(node->renderData.localRect.x, node->renderData.localRect.y, node->renderData.localRect.width, node->renderData.localRect.height);
|
||||
result.globalTransform = lottie::nativeTransform(node->renderData.globalTransform);
|
||||
result.drawsContent = node->renderData.drawsContent;
|
||||
result.hasSimpleContents = node->renderData.drawContentDescendants <= 1;
|
||||
result.drawContentDescendants = node->renderData.drawContentDescendants;
|
||||
result.isInvertedMatte = node->renderData.isInvertedMatte;
|
||||
if (node->mask()) {
|
||||
result.maskId = (int64_t)node->mask().get();
|
||||
} else {
|
||||
result.maskId = 0;
|
||||
}
|
||||
result.subnodeCount = (int)node->subnodes().size();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)) {
|
||||
lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId;
|
||||
return [self getRenderNodeProxyById:(int64_t)node->subnodes()[index].get()];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation LottieAnimationContainer (Internal)
|
||||
|
@ -52,6 +52,59 @@ private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount:
|
||||
return device.makeTexture(descriptor: textureDescriptor)!
|
||||
}
|
||||
|
||||
public func cacheLottieMetalAnimation(path: String) -> Data? {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
|
||||
if let lottieAnimation = LottieAnimation(data: decompressedData) {
|
||||
let animationContainer = LottieAnimationContainer(animation: lottieAnimation)
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
let buffer = WriteBuffer()
|
||||
var frameMapping = SerializedLottieMetalFrameMapping()
|
||||
frameMapping.size = animationContainer.animation.size
|
||||
frameMapping.frameCount = animationContainer.animation.frameCount
|
||||
frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond
|
||||
for i in 0 ..< frameMapping.frameCount {
|
||||
frameMapping.frameRanges[i] = 0 ..< 1
|
||||
}
|
||||
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
|
||||
|
||||
for i in 0 ..< animationContainer.animation.frameCount {
|
||||
animationContainer.update(i)
|
||||
let frameRangeStart = buffer.length
|
||||
if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) {
|
||||
serializeNode(buffer: buffer, node: node)
|
||||
let frameRangeEnd = buffer.length
|
||||
frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd
|
||||
}
|
||||
}
|
||||
|
||||
let previousLength = buffer.length
|
||||
buffer.length = 0
|
||||
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
|
||||
buffer.length = previousLength
|
||||
|
||||
buffer.trim()
|
||||
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
|
||||
let zippedData = TGGZipData(buffer.data, 1.0)
|
||||
print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
|
||||
|
||||
return zippedData
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseCachedLottieMetalAnimation(data: Data) -> LottieContentLayer.Content? {
|
||||
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
|
||||
let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
|
||||
let serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData)
|
||||
return .serialized(frameMapping: serializedFrames.0, data: serializedFrames.1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private final class AnimationCacheState {
|
||||
static let shared = AnimationCacheState()
|
||||
|
||||
@ -118,45 +171,8 @@ private final class AnimationCacheState {
|
||||
let cachePath = task.cachePath
|
||||
let queue = self.queue
|
||||
Queue.concurrentDefaultQueue().async { [weak self, weak task] in
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
|
||||
if let lottieAnimation = LottieAnimation(data: decompressedData) {
|
||||
let animationContainer = LottieAnimationContainer(animation: lottieAnimation)
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
let buffer = WriteBuffer()
|
||||
var frameMapping = SerializedFrameMapping()
|
||||
frameMapping.size = animationContainer.animation.size
|
||||
frameMapping.frameCount = animationContainer.animation.frameCount
|
||||
frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond
|
||||
for i in 0 ..< frameMapping.frameCount {
|
||||
frameMapping.frameRanges[i] = 0 ..< 1
|
||||
}
|
||||
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
|
||||
|
||||
for i in 0 ..< animationContainer.animation.frameCount {
|
||||
animationContainer.update(i)
|
||||
let frameRangeStart = buffer.length
|
||||
if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) {
|
||||
serializeNode(buffer: buffer, node: node)
|
||||
let frameRangeEnd = buffer.length
|
||||
frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd
|
||||
}
|
||||
}
|
||||
|
||||
let previousLength = buffer.length
|
||||
buffer.length = 0
|
||||
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
|
||||
buffer.length = previousLength
|
||||
|
||||
buffer.trim()
|
||||
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
|
||||
let zippedData = TGGZipData(buffer.data, 1.0)
|
||||
print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
|
||||
|
||||
let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic)
|
||||
}
|
||||
if let zippedData = cacheLottieMetalAnimation(path: path) {
|
||||
let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic)
|
||||
}
|
||||
|
||||
queue.async {
|
||||
@ -191,12 +207,371 @@ private final class AnimationCacheState {
|
||||
}
|
||||
}
|
||||
|
||||
private func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
|
||||
transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
|
||||
transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
|
||||
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
|
||||
|
||||
return transform
|
||||
}
|
||||
|
||||
private final class RenderFrameState {
|
||||
let canvasSize: CGSize
|
||||
let frameState: PathFrameState
|
||||
let currentBezierIndicesBuffer: PathRenderBuffer
|
||||
let currentBuffer: PathRenderBuffer
|
||||
|
||||
var transform: CATransform3D
|
||||
|
||||
init(
|
||||
canvasSize: CGSize,
|
||||
frameState: PathFrameState,
|
||||
currentBezierIndicesBuffer: PathRenderBuffer,
|
||||
currentBuffer: PathRenderBuffer
|
||||
) {
|
||||
self.canvasSize = canvasSize
|
||||
self.frameState = frameState
|
||||
self.currentBezierIndicesBuffer = currentBezierIndicesBuffer
|
||||
self.currentBuffer = currentBuffer
|
||||
|
||||
self.transform = defaultTransformForSize(canvasSize)
|
||||
}
|
||||
|
||||
var transformStack: [CATransform3D] = []
|
||||
|
||||
func saveState() {
|
||||
transformStack.append(transform)
|
||||
}
|
||||
|
||||
func restoreState() {
|
||||
transform = transformStack.removeLast()
|
||||
}
|
||||
|
||||
func concat(_ other: CATransform3D) {
|
||||
transform = CATransform3DConcat(other, transform)
|
||||
}
|
||||
|
||||
private func fillPath(path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
|
||||
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
|
||||
|
||||
path.enumerateItems { pathItem in
|
||||
switch pathItem.pointee.type {
|
||||
case .moveTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .lineTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .curveTo:
|
||||
let cp1 = pathItem.pointee.points.0
|
||||
let cp2 = pathItem.pointee.points.1
|
||||
let point = pathItem.pointee.points.2
|
||||
|
||||
fillState.addCurve(
|
||||
to: SIMD2<Float>(Float(point.x), Float(point.y)),
|
||||
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
|
||||
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
|
||||
)
|
||||
case .close:
|
||||
fillState.close()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fillState.close()
|
||||
|
||||
self.frameState.add(fill: fillState)
|
||||
}
|
||||
|
||||
private func strokePath(path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
|
||||
let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
|
||||
|
||||
path.enumerateItems { pathItem in
|
||||
switch pathItem.pointee.type {
|
||||
case .moveTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .lineTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .curveTo:
|
||||
let cp1 = pathItem.pointee.points.0
|
||||
let cp2 = pathItem.pointee.points.1
|
||||
let point = pathItem.pointee.points.2
|
||||
|
||||
strokeState.addCurve(
|
||||
to: SIMD2<Float>(Float(point.x), Float(point.y)),
|
||||
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
|
||||
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
|
||||
)
|
||||
case .close:
|
||||
strokeState.close()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
strokeState.complete()
|
||||
|
||||
self.frameState.add(stroke: strokeState)
|
||||
}
|
||||
|
||||
func renderNodeContent(item: LottieRenderContent, alpha: Double) {
|
||||
if let fill = item.fill {
|
||||
if let solidShading = fill.shading as? LottieRenderContentSolidShading {
|
||||
self.fillPath(
|
||||
path: item.path,
|
||||
shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
|
||||
rule: fill.fillRule,
|
||||
transform: transform
|
||||
)
|
||||
} else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
|
||||
let gradientType: PathShading.Gradient.GradientType
|
||||
switch gradientShading.gradientType {
|
||||
case .linear:
|
||||
gradientType = .linear
|
||||
case .radial:
|
||||
gradientType = .radial
|
||||
@unknown default:
|
||||
gradientType = .linear
|
||||
}
|
||||
var colorStops: [PathShading.Gradient.ColorStop] = []
|
||||
for colorStop in gradientShading.colorStops {
|
||||
colorStops.append(PathShading.Gradient.ColorStop(
|
||||
color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
|
||||
location: Float(colorStop.location)
|
||||
))
|
||||
}
|
||||
let gradientShading = PathShading.Gradient(
|
||||
gradientType: gradientType,
|
||||
colorStops: colorStops,
|
||||
start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
|
||||
end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
|
||||
)
|
||||
self.fillPath(
|
||||
path: item.path,
|
||||
shading: .gradient(gradientShading),
|
||||
rule: fill.fillRule,
|
||||
transform: transform
|
||||
)
|
||||
}
|
||||
} else if let stroke = item.stroke {
|
||||
if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
|
||||
let color = solidShading.color
|
||||
strokePath(
|
||||
path: item.path,
|
||||
width: stroke.lineWidth,
|
||||
join: stroke.lineJoin,
|
||||
cap: stroke.lineCap,
|
||||
miterLimit: stroke.miterLimit,
|
||||
color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
|
||||
transform: transform
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderNode(node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
|
||||
let normalizedOpacity = node.opacity
|
||||
let layerAlpha = normalizedOpacity * parentAlpha
|
||||
|
||||
if node.isHidden || normalizedOpacity == 0.0 {
|
||||
return
|
||||
}
|
||||
|
||||
saveState()
|
||||
|
||||
var needsTempContext = false
|
||||
if node.mask != nil {
|
||||
needsTempContext = true
|
||||
} else {
|
||||
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
|
||||
}
|
||||
|
||||
var maskSurface: PathFrameState.MaskSurface?
|
||||
|
||||
if needsTempContext {
|
||||
if node.mask != nil || node.masksToBounds {
|
||||
var maskMode: PathFrameState.MaskSurface.Mode = .regular
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
|
||||
if node.masksToBounds {
|
||||
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
|
||||
|
||||
fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
|
||||
fillState.close()
|
||||
|
||||
frameState.add(fill: fillState)
|
||||
}
|
||||
if let maskNode = node.mask {
|
||||
if maskNode.isInvertedMatte {
|
||||
maskMode = .inverse
|
||||
}
|
||||
renderNode(node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
|
||||
maskSurface = frameState.popOffscreenMask(mode: maskMode)
|
||||
}
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
} else {
|
||||
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
|
||||
concat(node.transform)
|
||||
}
|
||||
|
||||
var renderAlpha: CGFloat = 1.0
|
||||
if needsTempContext {
|
||||
renderAlpha = 1.0
|
||||
} else {
|
||||
renderAlpha = layerAlpha
|
||||
}
|
||||
|
||||
if let renderContent = node.renderContent {
|
||||
renderNodeContent(item: renderContent, alpha: renderAlpha)
|
||||
}
|
||||
|
||||
for subnode in node.subnodes {
|
||||
renderNode(node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
|
||||
}
|
||||
|
||||
if needsTempContext {
|
||||
restoreState()
|
||||
|
||||
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
|
||||
concat(node.transform)
|
||||
concat(CATransform3DInvert(node.globalTransform))
|
||||
|
||||
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
}
|
||||
|
||||
func renderNode(animationContainer: LottieAnimationContainer, node: LottieRenderNodeProxy, globalSize: CGSize, parentAlpha: CGFloat) {
|
||||
let normalizedOpacity = node.layer.opacity
|
||||
let layerAlpha = normalizedOpacity * parentAlpha
|
||||
|
||||
if node.layer.isHidden || normalizedOpacity == 0.0 {
|
||||
return
|
||||
}
|
||||
|
||||
saveState()
|
||||
|
||||
var needsTempContext = false
|
||||
if node.maskId != 0 {
|
||||
needsTempContext = true
|
||||
} else {
|
||||
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.layer.masksToBounds
|
||||
}
|
||||
|
||||
var maskSurface: PathFrameState.MaskSurface?
|
||||
|
||||
if needsTempContext {
|
||||
if node.maskId != 0 || node.layer.masksToBounds {
|
||||
var maskMode: PathFrameState.MaskSurface.Mode = .regular
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
|
||||
if node.layer.masksToBounds {
|
||||
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
|
||||
|
||||
fillState.begin(point: SIMD2<Float>(Float(node.layer.bounds.minX), Float(node.layer.bounds.minY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.minX), Float(node.layer.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.maxX), Float(node.layer.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.maxX), Float(node.layer.bounds.minY)))
|
||||
fillState.close()
|
||||
|
||||
frameState.add(fill: fillState)
|
||||
}
|
||||
if node.maskId != 0 {
|
||||
let maskNode = animationContainer.getRenderNodeProxy(byId: node.maskId)
|
||||
if maskNode.isInvertedMatte {
|
||||
maskMode = .inverse
|
||||
}
|
||||
renderNode(animationContainer: animationContainer, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
|
||||
maskSurface = frameState.popOffscreenMask(mode: maskMode)
|
||||
}
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
} else {
|
||||
concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0))
|
||||
concat(node.layer.transform)
|
||||
}
|
||||
|
||||
var renderAlpha: CGFloat = 1.0
|
||||
if needsTempContext {
|
||||
renderAlpha = 1.0
|
||||
} else {
|
||||
renderAlpha = layerAlpha
|
||||
}
|
||||
|
||||
/*if let renderContent = node.renderContent {
|
||||
renderNodeContent(item: renderContent, alpha: renderAlpha)
|
||||
}*/
|
||||
assert(false)
|
||||
|
||||
for i in 0 ..< node.subnodeCount {
|
||||
let subnode = animationContainer.getRenderNodeSubnodeProxy(byId: node.internalId, index: i)
|
||||
renderNode(animationContainer: animationContainer, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
|
||||
}
|
||||
|
||||
if needsTempContext {
|
||||
restoreState()
|
||||
|
||||
concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0))
|
||||
concat(node.layer.transform)
|
||||
concat(CATransform3DInvert(node.globalTransform))
|
||||
|
||||
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
}
|
||||
}
|
||||
|
||||
public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
enum Content {
|
||||
case serialized(frameMapping: SerializedFrameMapping, data: Data)
|
||||
public enum Content {
|
||||
case serialized(frameMapping: SerializedLottieMetalFrameMapping, data: Data)
|
||||
case animation(LottieAnimationContainer)
|
||||
|
||||
var size: CGSize {
|
||||
public var size: CGSize {
|
||||
switch self {
|
||||
case let .serialized(frameMapping, _):
|
||||
return frameMapping.size
|
||||
@ -205,7 +580,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
}
|
||||
}
|
||||
|
||||
var frameCount: Int {
|
||||
public var frameCount: Int {
|
||||
switch self {
|
||||
case let .serialized(frameMapping, _):
|
||||
return frameMapping.frameCount
|
||||
@ -214,7 +589,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
}
|
||||
}
|
||||
|
||||
var framesPerSecond: Int {
|
||||
public var framesPerSecond: Int {
|
||||
switch self {
|
||||
case let .serialized(frameMapping, _):
|
||||
return frameMapping.framesPerSecond
|
||||
@ -288,7 +663,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
}
|
||||
}
|
||||
|
||||
init(content: Content) {
|
||||
public init(content: Content) {
|
||||
self.content = content
|
||||
|
||||
super.init()
|
||||
@ -312,71 +687,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
|
||||
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
|
||||
|
||||
path.enumerateItems { pathItem in
|
||||
switch pathItem.pointee.type {
|
||||
case .moveTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .lineTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .curveTo:
|
||||
let cp1 = pathItem.pointee.points.0
|
||||
let cp2 = pathItem.pointee.points.1
|
||||
let point = pathItem.pointee.points.2
|
||||
|
||||
fillState.addCurve(
|
||||
to: SIMD2<Float>(Float(point.x), Float(point.y)),
|
||||
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
|
||||
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
|
||||
)
|
||||
case .close:
|
||||
fillState.close()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fillState.close()
|
||||
|
||||
frameState.add(fill: fillState)
|
||||
}
|
||||
|
||||
private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
|
||||
let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
|
||||
|
||||
path.enumerateItems { pathItem in
|
||||
switch pathItem.pointee.type {
|
||||
case .moveTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .lineTo:
|
||||
let point = pathItem.pointee.points.0
|
||||
strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
|
||||
case .curveTo:
|
||||
let cp1 = pathItem.pointee.points.0
|
||||
let cp2 = pathItem.pointee.points.1
|
||||
let point = pathItem.pointee.points.2
|
||||
|
||||
strokeState.addCurve(
|
||||
to: SIMD2<Float>(Float(point.x), Float(point.y)),
|
||||
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
|
||||
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
|
||||
)
|
||||
case .close:
|
||||
strokeState.close()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
strokeState.complete()
|
||||
|
||||
frameState.add(stroke: strokeState)
|
||||
}
|
||||
private var renderNodeCache: [Int: LottieRenderNode] = [:]
|
||||
|
||||
public func update(context: MetalEngineSubjectContext) {
|
||||
if self.bounds.isEmpty {
|
||||
@ -392,196 +703,32 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
return
|
||||
}
|
||||
|
||||
guard let node = content.updateAndGetRenderNode(frameIndex: self.frameIndex) else {
|
||||
var maybeNode: LottieRenderNode?
|
||||
if let current = self.renderNodeCache[self.frameIndex] {
|
||||
maybeNode = current
|
||||
} else {
|
||||
if let value = content.updateAndGetRenderNode(frameIndex: self.frameIndex) {
|
||||
maybeNode = value
|
||||
//self.renderNodeCache[self.frameIndex] = value
|
||||
}
|
||||
}
|
||||
guard let node = maybeNode else {
|
||||
return
|
||||
}
|
||||
|
||||
func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
|
||||
transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
|
||||
transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
|
||||
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
|
||||
|
||||
return transform
|
||||
}
|
||||
|
||||
let canvasSize = size
|
||||
var transform = defaultTransformForSize(canvasSize)
|
||||
|
||||
concat(CATransform3DMakeScale(canvasSize.width / content.size.width, canvasSize.height / content.size.height, 1.0))
|
||||
|
||||
var transformStack: [CATransform3D] = []
|
||||
|
||||
func saveState() {
|
||||
transformStack.append(transform)
|
||||
}
|
||||
|
||||
func restoreState() {
|
||||
transform = transformStack.removeLast()
|
||||
}
|
||||
|
||||
func concat(_ other: CATransform3D) {
|
||||
transform = CATransform3DConcat(other, transform)
|
||||
}
|
||||
|
||||
func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) {
|
||||
if let fill = item.fill {
|
||||
if let solidShading = fill.shading as? LottieRenderContentSolidShading {
|
||||
self.fillPath(
|
||||
frameState: frameState,
|
||||
path: item.path,
|
||||
shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
|
||||
rule: fill.fillRule,
|
||||
transform: transform
|
||||
)
|
||||
} else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
|
||||
let gradientType: PathShading.Gradient.GradientType
|
||||
switch gradientShading.gradientType {
|
||||
case .linear:
|
||||
gradientType = .linear
|
||||
case .radial:
|
||||
gradientType = .radial
|
||||
@unknown default:
|
||||
gradientType = .linear
|
||||
}
|
||||
var colorStops: [PathShading.Gradient.ColorStop] = []
|
||||
for colorStop in gradientShading.colorStops {
|
||||
colorStops.append(PathShading.Gradient.ColorStop(
|
||||
color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
|
||||
location: Float(colorStop.location)
|
||||
))
|
||||
}
|
||||
let gradientShading = PathShading.Gradient(
|
||||
gradientType: gradientType,
|
||||
colorStops: colorStops,
|
||||
start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
|
||||
end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
|
||||
)
|
||||
self.fillPath(
|
||||
frameState: frameState,
|
||||
path: item.path,
|
||||
shading: .gradient(gradientShading),
|
||||
rule: fill.fillRule,
|
||||
transform: transform
|
||||
)
|
||||
}
|
||||
} else if let stroke = item.stroke {
|
||||
if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
|
||||
let color = solidShading.color
|
||||
strokePath(
|
||||
frameState: frameState,
|
||||
path: item.path,
|
||||
width: stroke.lineWidth,
|
||||
join: stroke.lineJoin,
|
||||
cap: stroke.lineCap,
|
||||
miterLimit: stroke.miterLimit,
|
||||
color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
|
||||
transform: transform
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
|
||||
let normalizedOpacity = node.opacity
|
||||
let layerAlpha = normalizedOpacity * parentAlpha
|
||||
|
||||
if node.isHidden || normalizedOpacity == 0.0 {
|
||||
return
|
||||
}
|
||||
|
||||
saveState()
|
||||
|
||||
var needsTempContext = false
|
||||
if node.mask != nil {
|
||||
needsTempContext = true
|
||||
} else {
|
||||
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
|
||||
}
|
||||
|
||||
var maskSurface: PathFrameState.MaskSurface?
|
||||
|
||||
if needsTempContext {
|
||||
if node.mask != nil || node.masksToBounds {
|
||||
var maskMode: PathFrameState.MaskSurface.Mode = .regular
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
|
||||
if node.masksToBounds {
|
||||
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
|
||||
|
||||
fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
|
||||
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
|
||||
fillState.close()
|
||||
|
||||
frameState.add(fill: fillState)
|
||||
}
|
||||
if let maskNode = node.mask {
|
||||
if maskNode.isInvertedMatte {
|
||||
maskMode = .inverse
|
||||
}
|
||||
renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
|
||||
maskSurface = frameState.popOffscreenMask(mode: maskMode)
|
||||
}
|
||||
|
||||
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
|
||||
saveState()
|
||||
|
||||
transform = defaultTransformForSize(node.globalRect.size)
|
||||
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
|
||||
concat(node.globalTransform)
|
||||
} else {
|
||||
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
|
||||
concat(node.transform)
|
||||
}
|
||||
|
||||
var renderAlpha: CGFloat = 1.0
|
||||
if needsTempContext {
|
||||
renderAlpha = 1.0
|
||||
} else {
|
||||
renderAlpha = layerAlpha
|
||||
}
|
||||
|
||||
if let renderContent = node.renderContent {
|
||||
renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha)
|
||||
}
|
||||
|
||||
for subnode in node.subnodes {
|
||||
renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
|
||||
}
|
||||
|
||||
if needsTempContext {
|
||||
restoreState()
|
||||
|
||||
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
|
||||
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
|
||||
concat(node.transform)
|
||||
concat(CATransform3DInvert(node.globalTransform))
|
||||
|
||||
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
|
||||
}
|
||||
|
||||
restoreState()
|
||||
}
|
||||
|
||||
self.currentBuffer.reset()
|
||||
self.currentBezierIndicesBuffer.reset()
|
||||
let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: self.msaaSampleCount, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer)
|
||||
|
||||
renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0)
|
||||
let frameContext = RenderFrameState(
|
||||
canvasSize: size,
|
||||
frameState: frameState,
|
||||
currentBezierIndicesBuffer: self.currentBezierIndicesBuffer,
|
||||
currentBuffer: self.currentBuffer
|
||||
)
|
||||
frameContext.concat(CATransform3DMakeScale(frameContext.canvasSize.width / content.size.width, frameContext.canvasSize.height / content.size.height, 1.0))
|
||||
|
||||
frameContext.renderNode(node: node, globalSize: frameContext.canvasSize, parentAlpha: 1.0)
|
||||
|
||||
final class ComputeOutput {
|
||||
let pathRenderContext: PathRenderContext
|
||||
@ -693,7 +840,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
self.offscreenHeap = offscreenHeap
|
||||
}
|
||||
|
||||
frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize)
|
||||
frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: frameContext.canvasSize)
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
self.multisampleTextureQueue.append(multisampleTexture)
|
||||
@ -701,7 +848,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
||||
return nil
|
||||
}
|
||||
|
||||
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize)
|
||||
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: frameContext.canvasSize)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
@ -866,15 +1013,15 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
|
||||
return
|
||||
}
|
||||
|
||||
var serializedFrames: (SerializedFrameMapping, Data)?
|
||||
var serializedFrames: (SerializedLottieMetalFrameMapping, Data)?
|
||||
var cachePathValue: String?
|
||||
if let cachePathPrefix {
|
||||
let cachePath = cachePathPrefix + "-metal1"
|
||||
cachePathValue = cachePath
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: cachePath), options: .mappedIfSafe) {
|
||||
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
|
||||
let serializedFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
|
||||
serializedFrames = (serializedFrameMapping, unzippedData)
|
||||
let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
|
||||
serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -484,14 +484,14 @@ func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode {
|
||||
)
|
||||
}
|
||||
|
||||
struct SerializedFrameMapping {
|
||||
public struct SerializedLottieMetalFrameMapping {
|
||||
var size: CGSize = CGSize()
|
||||
var frameCount: Int = 0
|
||||
var framesPerSecond: Int = 0
|
||||
var frameRanges: [Int: Range<Int>] = [:]
|
||||
}
|
||||
|
||||
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMapping) {
|
||||
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedLottieMetalFrameMapping) {
|
||||
buffer.write(size: frameMapping.size)
|
||||
buffer.write(uInt32: UInt32(frameMapping.frameCount))
|
||||
buffer.write(uInt32: UInt32(frameMapping.framesPerSecond))
|
||||
@ -502,8 +502,8 @@ func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMap
|
||||
}
|
||||
}
|
||||
|
||||
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedFrameMapping {
|
||||
var frameMapping = SerializedFrameMapping()
|
||||
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedLottieMetalFrameMapping {
|
||||
var frameMapping = SerializedLottieMetalFrameMapping()
|
||||
|
||||
frameMapping.size = buffer.readSize()
|
||||
frameMapping.frameCount = Int(buffer.readUInt32())
|
||||
|
@ -4450,7 +4450,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme),
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||
selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
|
||||
title: self.displayLikeReactions ? nil : (isGroup ? component.strings.Story_SendReactionAsGroupMessage : component.strings.Story_SendReactionAsMessage),
|
||||
reactionsLocked: false,
|
||||
|
@ -111,7 +111,7 @@ extension ChatControllerImpl {
|
||||
actions.animationCache = self.controllerInteraction?.presentationContext.animationCache
|
||||
|
||||
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
|
||||
actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
|
||||
actions.reactionItems = topReactions.map { ReactionContextItem.reaction(item: $0, icon: .none) }
|
||||
actions.selectedReactionItems = selectedReactions.reactions
|
||||
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
|
||||
if self.presentationInterfaceState.isPremium {
|
||||
@ -131,7 +131,7 @@ extension ChatControllerImpl {
|
||||
if !actions.reactionItems.isEmpty {
|
||||
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
|
||||
switch item {
|
||||
case let .reaction(reaction):
|
||||
case let .reaction(reaction, _):
|
||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||
default:
|
||||
return nil
|
||||
|
@ -45,11 +45,22 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
||||
effectItems = .single(nil)
|
||||
}
|
||||
|
||||
let availableMessageEffects = selfController.context.availableMessageEffects |> take(1)
|
||||
let hasPremium = selfController.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfController.context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|
||||
let _ = (combineLatest(
|
||||
selfController.context.account.viewTracker.peerView(peerId) |> take(1),
|
||||
effectItems
|
||||
effectItems,
|
||||
availableMessageEffects,
|
||||
hasPremium
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems in
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium in
|
||||
guard let selfController, let peer = peerViewMainPeer(peerView) else {
|
||||
return
|
||||
}
|
||||
@ -98,7 +109,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
||||
return
|
||||
}
|
||||
selfController.controllerInteraction?.scheduleCurrentMessage()
|
||||
}, reactionItems: effectItems)
|
||||
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
|
||||
selfController.sendMessageActionsController = controller
|
||||
if layout.isNonExclusive {
|
||||
selfController.present(controller, in: .window(.root))
|
||||
|
@ -59,6 +59,8 @@ public protocol WallpaperBubbleBackgroundNode: ASDisplayNode {
|
||||
func update(rect: CGRect, within containerSize: CGSize, animator: ControlledTransitionAnimator)
|
||||
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double)
|
||||
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat)
|
||||
|
||||
func reloadBindings()
|
||||
}
|
||||
|
||||
public enum WallpaperDisplayMode {
|
||||
@ -323,7 +325,7 @@ private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOver
|
||||
}
|
||||
}
|
||||
|
||||
final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode {
|
||||
public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode {
|
||||
final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
|
||||
var implicitContentUpdate: Bool = true
|
||||
|
||||
@ -631,6 +633,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
gradientWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
func reloadBindings() {
|
||||
}
|
||||
}
|
||||
|
||||
final class BubbleBackgroundPortalNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
|
||||
@ -679,6 +684,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
|
||||
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
||||
}
|
||||
|
||||
func reloadBindings() {
|
||||
self.portalView.reloadPortal()
|
||||
}
|
||||
}
|
||||
|
||||
private final class BubbleBackgroundNodeReference {
|
||||
@ -812,7 +821,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
}
|
||||
}
|
||||
|
||||
var rotation: CGFloat = 0.0 {
|
||||
public var rotation: CGFloat = 0.0 {
|
||||
didSet {
|
||||
var fromValue: CGFloat = 0.0
|
||||
if let value = (self.layer.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue {
|
||||
@ -845,7 +854,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
private static var cachedSharedPattern: (PatternKey, UIImage)?
|
||||
|
||||
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
var isReady: Signal<Bool, NoError> {
|
||||
public var isReady: Signal<Bool, NoError> {
|
||||
return self._isReady.get()
|
||||
}
|
||||
|
||||
@ -920,7 +929,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
self.dimLayer.opacity = dimAlpha
|
||||
}
|
||||
|
||||
func update(wallpaper: TelegramWallpaper, animated: Bool) {
|
||||
public func update(wallpaper: TelegramWallpaper, animated: Bool) {
|
||||
if self.wallpaper == wallpaper {
|
||||
return
|
||||
}
|
||||
@ -1074,7 +1083,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
self.updateDimming()
|
||||
}
|
||||
|
||||
func _internalUpdateIsSettingUpWallpaper() {
|
||||
public func _internalUpdateIsSettingUpWallpaper() {
|
||||
self.isSettingUpWallpaper = true
|
||||
}
|
||||
|
||||
@ -1301,7 +1310,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) {
|
||||
public func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstLayout = self.validLayout == nil
|
||||
self.validLayout = (size, displayMode)
|
||||
|
||||
@ -1357,7 +1366,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
private var isAnimating = false
|
||||
private var isLooping = false
|
||||
|
||||
func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
|
||||
public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
|
||||
guard !(self.isLooping && self.isAnimating) else {
|
||||
return
|
||||
}
|
||||
@ -1373,7 +1382,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {})
|
||||
}
|
||||
|
||||
func updateIsLooping(_ isLooping: Bool) {
|
||||
public func updateIsLooping(_ isLooping: Bool) {
|
||||
let wasLooping = self.isLooping
|
||||
self.isLooping = isLooping
|
||||
|
||||
@ -1382,7 +1391,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
}
|
||||
}
|
||||
|
||||
func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) {
|
||||
public func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) {
|
||||
if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners {
|
||||
self.bubbleTheme = bubbleTheme
|
||||
self.bubbleCorners = bubbleCorners
|
||||
@ -1430,7 +1439,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
}
|
||||
}
|
||||
|
||||
func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool {
|
||||
public func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool {
|
||||
guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else {
|
||||
return false
|
||||
}
|
||||
@ -1474,13 +1483,18 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func makeLegacyBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
|
||||
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
|
||||
node.updateContents()
|
||||
return node
|
||||
}
|
||||
|
||||
func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
|
||||
public func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
|
||||
if !self.hasBubbleBackground(for: type) {
|
||||
return nil
|
||||
}
|
||||
|
||||
#if true
|
||||
var sourceView: PortalSourceView?
|
||||
switch type {
|
||||
case .free:
|
||||
@ -1499,14 +1513,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
|
||||
return node
|
||||
}
|
||||
#else
|
||||
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
|
||||
node.updateContents()
|
||||
return node
|
||||
#endif
|
||||
}
|
||||
|
||||
func makeFreeBackground() -> PortalView? {
|
||||
public func makeFreeBackground() -> PortalView? {
|
||||
if !self.hasBubbleBackground(for: .free) {
|
||||
return nil
|
||||
}
|
||||
@ -1519,7 +1528,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
}
|
||||
}
|
||||
|
||||
func hasExtraBubbleBackground() -> Bool {
|
||||
public func hasExtraBubbleBackground() -> Bool {
|
||||
var isInvertedGradient = false
|
||||
switch self.wallpaper {
|
||||
case let .file(file):
|
||||
@ -1532,7 +1541,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
||||
return isInvertedGradient
|
||||
}
|
||||
|
||||
func makeDimmedNode() -> ASDisplayNode? {
|
||||
public func makeDimmedNode() -> ASDisplayNode? {
|
||||
if let gradientBackgroundNode = self.gradientBackgroundNode {
|
||||
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user