mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +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
|
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 {
|
private func processAnimationFolderItems(items: [(String, String)], countPerBucket: Int, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
||||||
let bucketCount = items.count / countPerBucket
|
let bucketCount = items.count / countPerBucket
|
||||||
var buckets: [[(String, String)]] = []
|
var buckets: [[(String, String)]] = []
|
||||||
@ -253,6 +254,7 @@ private func processAnimationFolderItemsParallel(items: [(String, String)], stop
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available (iOS 13.0, *)
|
||||||
func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
||||||
let items = buildAnimationFolderItems(basePath: basePath, path: path)
|
let items = buildAnimationFolderItems(basePath: basePath, path: path)
|
||||||
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)
|
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)
|
||||||
|
@ -110,6 +110,8 @@ public final class ViewController: UIViewController {
|
|||||||
override public func viewDidLoad() {
|
override public func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
SharedDisplayLinkDriver.shared.updateForegroundState(true)
|
||||||
|
|
||||||
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
||||||
let filePath = bundlePath + "/fireworks.json"
|
let filePath = bundlePath + "/fireworks.json"
|
||||||
|
|
||||||
@ -117,12 +119,15 @@ public final class ViewController: UIViewController {
|
|||||||
|
|
||||||
self.view.layer.addSublayer(MetalEngine.shared.rootLayer)
|
self.view.layer.addSublayer(MetalEngine.shared.rootLayer)
|
||||||
|
|
||||||
if "".isEmpty {
|
if !"".isEmpty {
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
self.test = ReferenceCompareTest(view: self.view)
|
self.test = ReferenceCompareTest(view: self.view)
|
||||||
}
|
}
|
||||||
} else if !"".isEmpty {
|
} else if "".isEmpty {
|
||||||
let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
|
let cachedAnimation = cacheLottieMetalAnimation(path: filePath)!
|
||||||
|
let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)!
|
||||||
|
|
||||||
|
/*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||||
|
|
||||||
var startTime = CFAbsoluteTimeGetCurrent()
|
var startTime = CFAbsoluteTimeGetCurrent()
|
||||||
let animation = LottieAnimation(data: animationData)!
|
let animation = LottieAnimation(data: animationData)!
|
||||||
@ -131,9 +136,9 @@ public final class ViewController: UIViewController {
|
|||||||
startTime = CFAbsoluteTimeGetCurrent()
|
startTime = CFAbsoluteTimeGetCurrent()
|
||||||
let animationContainer = LottieAnimationContainer(animation: animation)
|
let animationContainer = LottieAnimationContainer(animation: animation)
|
||||||
animationContainer.update(0)
|
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))
|
lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0))
|
||||||
self.view.layer.addSublayer(lottieLayer)
|
self.view.layer.addSublayer(lottieLayer)
|
||||||
lottieLayer.setNeedsUpdate()
|
lottieLayer.setNeedsUpdate()
|
||||||
@ -162,7 +167,7 @@ public final class ViewController: UIViewController {
|
|||||||
var frameIndex = 0
|
var frameIndex = 0
|
||||||
while true {
|
while true {
|
||||||
animationContainer.update(frameIndex)
|
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
|
frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount
|
||||||
numUpdates += 1
|
numUpdates += 1
|
||||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||||
|
@ -168,7 +168,7 @@ public protocol AnimatedStickerNode: ASDisplayNode {
|
|||||||
var visibility: Bool { get set }
|
var visibility: Bool { get set }
|
||||||
var overrideVisibility: 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 cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
|
||||||
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
|
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
|
||||||
|
@ -39,6 +39,8 @@ swift_library(
|
|||||||
"//submodules/TextFormat:TextFormat",
|
"//submodules/TextFormat:TextFormat",
|
||||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
|
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
|
||||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
||||||
|
"//submodules/ReactionSelectionNode",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -21,6 +21,8 @@ import ShimmerEffect
|
|||||||
import TextFormat
|
import TextFormat
|
||||||
import LegacyMessageInputPanel
|
import LegacyMessageInputPanel
|
||||||
import LegacyMessageInputPanelInputView
|
import LegacyMessageInputPanelInputView
|
||||||
|
import ReactionSelectionNode
|
||||||
|
import TopMessageReactions
|
||||||
|
|
||||||
private let buttonSize = CGSize(width: 88.0, height: 49.0)
|
private let buttonSize = CGSize(width: 88.0, height: 49.0)
|
||||||
private let smallButtonWidth: CGFloat = 69.0
|
private let smallButtonWidth: CGFloat = 69.0
|
||||||
@ -926,9 +928,31 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
if case .media = strongSelf.presentationInterfaceState.inputMode {
|
if case .media = strongSelf.presentationInterfaceState.inputMode {
|
||||||
hasEntityKeyboard = true
|
hasEntityKeyboard = true
|
||||||
}
|
}
|
||||||
let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
|
|
||||||
|> take(1)
|
let effectItems: Signal<[ReactionItem]?, NoError>
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
|
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 {
|
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -955,7 +979,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}, schedule: { [weak textInputPanelNode] _ in
|
}, schedule: { [weak textInputPanelNode] _ in
|
||||||
textInputPanelNode?.sendMessage(.schedule)
|
textInputPanelNode?.sendMessage(.schedule)
|
||||||
})
|
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
|
||||||
strongSelf.presentInGlobalOverlay(controller)
|
strongSelf.presentInGlobalOverlay(controller)
|
||||||
})
|
})
|
||||||
}, openScheduledMessages: {
|
}, openScheduledMessages: {
|
||||||
|
@ -492,7 +492,7 @@ public func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: Princ
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
||||||
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
public private(set) var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||||
|
|
||||||
private var currentType: ChatMessageBackgroundType?
|
private var currentType: ChatMessageBackgroundType?
|
||||||
private var currentMaskMode: Bool?
|
private var currentMaskMode: Bool?
|
||||||
|
@ -33,7 +33,10 @@ swift_library(
|
|||||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||||
"//submodules/Components/MultilineTextComponent",
|
"//submodules/Components/MultilineTextComponent",
|
||||||
"//submodules/TelegramUI/Components/LottieMetal",
|
"//submodules/TelegramUI/Components/LottieMetal",
|
||||||
|
"//submodules/AnimatedStickerNode",
|
||||||
"//submodules/TelegramAnimatedStickerNode",
|
"//submodules/TelegramAnimatedStickerNode",
|
||||||
|
"//submodules/ActivityIndicator",
|
||||||
|
"//submodules/UndoUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -210,7 +210,9 @@ public func makeChatSendMessageActionSheetController(
|
|||||||
completion: @escaping () -> Void,
|
completion: @escaping () -> Void,
|
||||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
reactionItems: [ReactionItem]? = nil
|
reactionItems: [ReactionItem]? = nil,
|
||||||
|
availableMessageEffects: AvailableMessageEffects? = nil,
|
||||||
|
isPremium: Bool = false
|
||||||
) -> ChatSendMessageActionSheetController {
|
) -> ChatSendMessageActionSheetController {
|
||||||
if textInputView.text.isEmpty {
|
if textInputView.text.isEmpty {
|
||||||
return ChatSendMessageActionSheetControllerImpl(
|
return ChatSendMessageActionSheetControllerImpl(
|
||||||
@ -229,7 +231,7 @@ public func makeChatSendMessageActionSheetController(
|
|||||||
completion: completion,
|
completion: completion,
|
||||||
sendMessage: sendMessage,
|
sendMessage: sendMessage,
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
reactionItems: reactionItems
|
reactionItems: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +252,8 @@ public func makeChatSendMessageActionSheetController(
|
|||||||
completion: completion,
|
completion: completion,
|
||||||
sendMessage: sendMessage,
|
sendMessage: sendMessage,
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
reactionItems: reactionItems
|
reactionItems: reactionItems,
|
||||||
|
availableMessageEffects: availableMessageEffects,
|
||||||
|
isPremium: isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -388,7 +388,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
|||||||
context: context,
|
context: context,
|
||||||
animationCache: context.animationCache,
|
animationCache: context.animationCache,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
items: reactionItems.map(ReactionContextItem.reaction),
|
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||||
selectedItems: Set(),
|
selectedItems: Set(),
|
||||||
title: "Add an animated effect",
|
title: "Add an animated effect",
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
|
@ -18,6 +18,9 @@ import ReactionSelectionNode
|
|||||||
import EntityKeyboard
|
import EntityKeyboard
|
||||||
import LottieMetal
|
import LottieMetal
|
||||||
import TelegramAnimatedStickerNode
|
import TelegramAnimatedStickerNode
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import ChatInputTextNode
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||||
@ -48,6 +51,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||||
let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||||
let reactionItems: [ReactionItem]?
|
let reactionItems: [ReactionItem]?
|
||||||
|
let availableMessageEffects: AvailableMessageEffects?
|
||||||
|
let isPremium: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -65,7 +70,9 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
completion: @escaping () -> Void,
|
completion: @escaping () -> Void,
|
||||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
reactionItems: [ReactionItem]?
|
reactionItems: [ReactionItem]?,
|
||||||
|
availableMessageEffects: AvailableMessageEffects?,
|
||||||
|
isPremium: Bool
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
@ -83,6 +90,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
self.sendMessage = sendMessage
|
self.sendMessage = sendMessage
|
||||||
self.schedule = schedule
|
self.schedule = schedule
|
||||||
self.reactionItems = reactionItems
|
self.reactionItems = reactionItems
|
||||||
|
self.availableMessageEffects = availableMessageEffects
|
||||||
|
self.isPremium = isPremium
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
||||||
@ -115,7 +124,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
private let backgroundView: BlurredBackgroundView
|
private let backgroundView: BlurredBackgroundView
|
||||||
|
|
||||||
private var sendButton: HighlightTrackingButton?
|
private var sendButton: SendButton?
|
||||||
private var messageItemView: MessageItemView?
|
private var messageItemView: MessageItemView?
|
||||||
private var actionsStackNode: ContextControllerActionsStackNode?
|
private var actionsStackNode: ContextControllerActionsStackNode?
|
||||||
private var reactionContextNode: ReactionContextNode?
|
private var reactionContextNode: ReactionContextNode?
|
||||||
@ -129,7 +138,10 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
|
|
||||||
private let messageEffectDisposable = MetaDisposable()
|
private let messageEffectDisposable = MetaDisposable()
|
||||||
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
|
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 presentationAnimationState: PresentationAnimationState = .initial
|
||||||
private var appliedAnimationState: PresentationAnimationState = .initial
|
private var appliedAnimationState: PresentationAnimationState = .initial
|
||||||
@ -164,6 +176,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.messageEffectDisposable.dispose()
|
self.messageEffectDisposable.dispose()
|
||||||
|
self.loadEffectAnimationDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) {
|
@objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
@ -195,6 +208,20 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
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 {
|
func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
@ -254,23 +281,32 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendButton: HighlightTrackingButton
|
let sendButton: SendButton
|
||||||
if let current = self.sendButton {
|
if let current = self.sendButton {
|
||||||
sendButton = current
|
sendButton = current
|
||||||
} else {
|
} else {
|
||||||
sendButton = HighlightTrackingButton()
|
sendButton = SendButton()
|
||||||
sendButton.accessibilityLabel = environment.strings.MediaPicker_Send
|
sendButton.accessibilityLabel = environment.strings.MediaPicker_Send
|
||||||
sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside)
|
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
|
snapshotView.isUserInteractionEnabled = false
|
||||||
sendButton.addSubview(snapshotView)
|
sendButton.addSubview(snapshotView)
|
||||||
}
|
}*/
|
||||||
self.sendButton = sendButton
|
self.sendButton = sendButton
|
||||||
self.addSubview(sendButton)
|
self.addSubview(sendButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
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
|
let sendButtonScale: CGFloat
|
||||||
switch self.presentationAnimationState {
|
switch self.presentationAnimationState {
|
||||||
case .initial:
|
case .initial:
|
||||||
@ -279,55 +315,6 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
sendButtonScale = 1.0
|
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
|
let actionsStackNode: ContextControllerActionsStackNode
|
||||||
if let current = self.actionsStackNode {
|
if let current = self.actionsStackNode {
|
||||||
actionsStackNode = current
|
actionsStackNode = current
|
||||||
@ -430,7 +417,73 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
presentation: .modal,
|
presentation: .modal,
|
||||||
transition: transition.containedViewLayoutTransition
|
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 {
|
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
|
||||||
let reactionContextNode: ReactionContextNode
|
let reactionContextNode: ReactionContextNode
|
||||||
@ -442,7 +495,21 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
animationCache: component.context.animationCache,
|
animationCache: component.context.animationCache,
|
||||||
presentationData: presentationData,
|
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(),
|
selectedItems: Set(),
|
||||||
title: "Add an animated effect",
|
title: "Add an animated effect",
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
@ -477,9 +544,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !self.isUpdating {
|
self.requestUpdateOverlayWantsToBeBelowKeyboard(transition: transition)
|
||||||
self.state?.updated(transition: Transition(transition))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||||
@ -506,11 +571,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.messageEffectDisposable.set((combineLatest(
|
self.messageEffectDisposable.set((messageEffect
|
||||||
messageEffect,
|
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect in
|
||||||
ReactionContextNode.randomGenericReactionEffect(context: component.context)
|
|
||||||
)
|
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
|
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -523,6 +585,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
if selectedMessageEffect.id == effectId {
|
if selectedMessageEffect.id == effectId {
|
||||||
self.selectedMessageEffect = nil
|
self.selectedMessageEffect = nil
|
||||||
reactionContextNode.selectedItems = Set([])
|
reactionContextNode.selectedItems = Set([])
|
||||||
|
self.loadEffectAnimationDisposable?.dispose()
|
||||||
|
self.isLoadingEffectAnimation = false
|
||||||
|
|
||||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
self.standaloneReactionAnimation = nil
|
self.standaloneReactionAnimation = nil
|
||||||
@ -541,6 +605,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HapticFeedback().tap()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.selectedMessageEffect = messageEffect
|
self.selectedMessageEffect = messageEffect
|
||||||
@ -548,10 +614,14 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HapticFeedback().tap()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let targetView = self.messageItemView?.effectIconView else {
|
self.loadEffectAnimationDisposable?.dispose()
|
||||||
return
|
self.isLoadingEffectAnimation = true
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
@ -561,56 +631,141 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = path
|
var customEffectResource: (FileMediaReference, MediaResource)?
|
||||||
|
|
||||||
var customEffectResource: MediaResource?
|
|
||||||
if let effectAnimation = messageEffect.effectAnimation {
|
if let effectAnimation = messageEffect.effectAnimation {
|
||||||
customEffectResource = effectAnimation.resource
|
customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource)
|
||||||
} else {
|
} else {
|
||||||
let effectSticker = messageEffect.effectSticker
|
let effectSticker = messageEffect.effectSticker
|
||||||
if let effectFile = effectSticker.videoThumbnails.first {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
|
let context = component.context
|
||||||
standaloneReactionAnimation.isUserInteractionEnabled = false
|
var loadEffectAnimationSignal: Signal<Never, NoError>
|
||||||
let effectSize = CGSize(width: 380.0, height: 380.0)
|
loadEffectAnimationSignal = Signal { subscriber in
|
||||||
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
|
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start()
|
||||||
effectFrame.origin.x -= effectFrame.width * 0.3
|
|
||||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource)
|
||||||
standaloneReactionAnimation.frame = effectFrame
|
|> filter { status in
|
||||||
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
|
if status == .Local {
|
||||||
self.addSubnode(standaloneReactionAnimation)
|
return true
|
||||||
|
} else {
|
||||||
let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id)
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|> 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.displayTail = true
|
||||||
reactionContextNode.forceTailToRight = false
|
reactionContextNode.forceTailToRight = false
|
||||||
reactionContextNode.forceDark = false
|
reactionContextNode.forceDark = false
|
||||||
|
reactionContextNode.isMessageEffects = true
|
||||||
self.reactionContextNode = reactionContextNode
|
self.reactionContextNode = reactionContextNode
|
||||||
self.addSubview(reactionContextNode.view)
|
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 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 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)
|
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
|
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 messageItemFrame: CGRect
|
||||||
let actionsStackFrame: CGRect
|
let actionsStackFrame: CGRect
|
||||||
let sendButtonFrame: CGRect
|
let sendButtonFrame: CGRect
|
||||||
@ -697,6 +859,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
|
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
|
||||||
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
|
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
|
||||||
transition.setScale(view: sendButton, scale: sendButtonScale)
|
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))
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||||
@ -769,6 +932,14 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
|||||||
private var processedDidAppear: Bool = false
|
private var processedDidAppear: Bool = false
|
||||||
private var processedDidDisappear: 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(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||||
@ -786,7 +957,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
|||||||
completion: @escaping () -> Void,
|
completion: @escaping () -> Void,
|
||||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||||
reactionItems: [ReactionItem]?
|
reactionItems: [ReactionItem]?,
|
||||||
|
availableMessageEffects: AvailableMessageEffects?,
|
||||||
|
isPremium: Bool
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -808,7 +981,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
|||||||
completion: completion,
|
completion: completion,
|
||||||
sendMessage: sendMessage,
|
sendMessage: sendMessage,
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
reactionItems: reactionItems
|
reactionItems: reactionItems,
|
||||||
|
availableMessageEffects: availableMessageEffects,
|
||||||
|
isPremium: isPremium
|
||||||
),
|
),
|
||||||
navigationBarAppearance: .none,
|
navigationBarAppearance: .none,
|
||||||
statusBarStyle: .none,
|
statusBarStyle: .none,
|
||||||
|
@ -17,6 +17,7 @@ import WallpaperBackgroundNode
|
|||||||
import MultilineTextWithEntitiesComponent
|
import MultilineTextWithEntitiesComponent
|
||||||
import ReactionButtonListComponent
|
import ReactionButtonListComponent
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
|
import ChatInputTextNode
|
||||||
|
|
||||||
private final class EffectIcon: Component {
|
private final class EffectIcon: Component {
|
||||||
enum Content: Equatable {
|
enum Content: Equatable {
|
||||||
@ -135,7 +136,8 @@ final class MessageItemView: UIView {
|
|||||||
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
||||||
private let backgroundNode: ChatMessageBackground
|
private let backgroundNode: ChatMessageBackground
|
||||||
|
|
||||||
private let text = ComponentView<Empty>()
|
private let textClippingContainer: UIView
|
||||||
|
private var textNode: ChatInputTextNode?
|
||||||
|
|
||||||
private var effectIcon: ComponentView<Empty>?
|
private var effectIcon: ComponentView<Empty>?
|
||||||
var effectIconView: UIView? {
|
var effectIconView: UIView? {
|
||||||
@ -150,10 +152,15 @@ final class MessageItemView: UIView {
|
|||||||
self.backgroundNode = ChatMessageBackground()
|
self.backgroundNode = ChatMessageBackground()
|
||||||
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
||||||
|
|
||||||
|
self.textClippingContainer = UIView()
|
||||||
|
self.textClippingContainer.clipsToBounds = true
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.backgroundWallpaperNode.view)
|
self.addSubview(self.backgroundWallpaperNode.view)
|
||||||
self.addSubview(self.backgroundNode.view)
|
self.addSubview(self.backgroundNode.view)
|
||||||
|
|
||||||
|
self.addSubview(self.textClippingContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init(coder: NSCoder) {
|
required init(coder: NSCoder) {
|
||||||
@ -165,9 +172,11 @@ final class MessageItemView: UIView {
|
|||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
backgroundNode: WallpaperBackgroundNode?,
|
backgroundNode: WallpaperBackgroundNode?,
|
||||||
textString: NSAttributedString,
|
textString: NSAttributedString,
|
||||||
|
sourceTextInputView: ChatInputTextView?,
|
||||||
textInsets: UIEdgeInsets,
|
textInsets: UIEdgeInsets,
|
||||||
explicitBackgroundSize: CGSize?,
|
explicitBackgroundSize: CGSize?,
|
||||||
maxTextWidth: CGFloat,
|
maxTextWidth: CGFloat,
|
||||||
|
maxTextHeight: CGFloat,
|
||||||
effect: AvailableMessageEffects.MessageEffect?,
|
effect: AvailableMessageEffects.MessageEffect?,
|
||||||
transition: Transition
|
transition: Transition
|
||||||
) -> CGSize {
|
) -> CGSize {
|
||||||
@ -201,33 +210,84 @@ final class MessageItemView: UIView {
|
|||||||
if let effectIconSize {
|
if let effectIconSize {
|
||||||
textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height))
|
textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height))
|
||||||
}
|
}
|
||||||
|
let _ = textCutout
|
||||||
|
|
||||||
let textSize = self.text.update(
|
let textNode: ChatInputTextNode
|
||||||
transition: .immediate,
|
if let current = self.textNode {
|
||||||
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
textNode = current
|
||||||
context: context,
|
} else {
|
||||||
animationCache: context.animationCache,
|
textNode = ChatInputTextNode(disableTiling: true)
|
||||||
animationRenderer: context.animationRenderer,
|
textNode.textView.isScrollEnabled = false
|
||||||
placeholderColor: presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper,
|
textNode.isUserInteractionEnabled = false
|
||||||
text: .plain(textString),
|
self.textNode = textNode
|
||||||
maximumNumberOfLines: 0,
|
self.textClippingContainer.addSubview(textNode.view)
|
||||||
lineSpacing: 0.0,
|
|
||||||
cutout: textCutout,
|
if let sourceTextInputView {
|
||||||
insets: UIEdgeInsets()
|
textNode.textView.defaultTextContainerInset = sourceTextInputView.defaultTextContainerInset
|
||||||
)),
|
}
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: maxTextWidth, height: 20000.0)
|
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)
|
var currentRightInset: CGFloat = 0.0
|
||||||
if let textView = self.text.view {
|
if let sourceTextInputView {
|
||||||
if textView.superview == nil {
|
currentRightInset = sourceTextInputView.currentRightInset
|
||||||
self.addSubview(textView)
|
|
||||||
}
|
|
||||||
textView.frame = textFrame
|
|
||||||
}
|
}
|
||||||
|
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
|
let chatTheme: ChatPresentationThemeData
|
||||||
if let current = self.chatTheme, current.theme === presentationData.theme {
|
if let current = self.chatTheme, current.theme === presentationData.theme {
|
||||||
@ -260,6 +320,21 @@ final class MessageItemView: UIView {
|
|||||||
let previousSize = self.currentSize
|
let previousSize = self.currentSize
|
||||||
self.currentSize = backgroundSize
|
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 effectIcon = self.effectIcon, let effectIconSize {
|
||||||
if let effectIconView = effectIcon.view {
|
if let effectIconView = effectIcon.view {
|
||||||
var animateIn = false
|
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))
|
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,
|
context: self.context,
|
||||||
animationCache: self.context.animationCache,
|
animationCache: self.context.animationCache,
|
||||||
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
|
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(),
|
selectedItems: Set(),
|
||||||
title: nil,
|
title: nil,
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
|
@ -41,6 +41,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
private let backgroundView: BlurredBackgroundView
|
private let backgroundView: BlurredBackgroundView
|
||||||
private(set) var vibrancyEffectView: UIVisualEffectView?
|
private(set) var vibrancyEffectView: UIVisualEffectView?
|
||||||
|
let vibrantExpandedContentContainer: UIView
|
||||||
|
|
||||||
private let maskLayer: SimpleLayer
|
private let maskLayer: SimpleLayer
|
||||||
private let backgroundClippingLayer: SimpleLayer
|
private let backgroundClippingLayer: SimpleLayer
|
||||||
@ -84,6 +85,8 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
|||||||
self.smallCircleLayer.cornerCurve = .circular
|
self.smallCircleLayer.cornerCurve = .circular
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.vibrantExpandedContentContainer = UIView()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.layer.addSublayer(self.backgroundShadowLayer)
|
self.layer.addSublayer(self.backgroundShadowLayer)
|
||||||
@ -146,6 +149,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
|||||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||||
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
||||||
self.vibrancyEffectView = vibrancyEffectView
|
self.vibrancyEffectView = vibrancyEffectView
|
||||||
|
vibrancyEffectView.contentView.addSubview(self.vibrantExpandedContentContainer)
|
||||||
self.backgroundView.addSubview(vibrancyEffectView)
|
self.backgroundView.addSubview(vibrancyEffectView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,9 +82,9 @@ public enum ReactionContextItem: Equatable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .reaction(lhsReaction):
|
case let .reaction(lhsReaction, lhsIcon):
|
||||||
if case let .reaction(rhsReaction) = rhs {
|
if case let .reaction(rhsReaction, rhsIcon) = rhs {
|
||||||
return lhsReaction.reaction == rhsReaction.reaction
|
return lhsReaction.reaction == rhsReaction.reaction && lhsIcon == rhsIcon
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -98,11 +98,11 @@ public enum ReactionContextItem: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case staticEmoji(String)
|
case staticEmoji(String)
|
||||||
case reaction(ReactionItem)
|
case reaction(item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon)
|
||||||
case premium
|
case premium
|
||||||
|
|
||||||
public var reaction: ReactionItem.Reaction? {
|
public var reaction: ReactionItem.Reaction? {
|
||||||
if case let .reaction(item) = self {
|
if case let .reaction(item, _) = self {
|
||||||
return item.reaction
|
return item.reaction
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
@ -386,6 +386,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
public var forceDark: Bool = false
|
public var forceDark: Bool = false
|
||||||
public var hideBackground: Bool = false
|
public var hideBackground: Bool = false
|
||||||
|
|
||||||
|
public var isMessageEffects: Bool = false
|
||||||
|
|
||||||
private var didAnimateIn: Bool = false
|
private var didAnimateIn: Bool = false
|
||||||
public private(set) var isAnimatingOut: Bool = false
|
public private(set) var isAnimatingOut: Bool = false
|
||||||
public private(set) var isAnimatingOutToReaction: Bool = false
|
public private(set) var isAnimatingOutToReaction: Bool = false
|
||||||
@ -829,6 +831,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
public func wantsDisplayBelowKeyboard() -> Bool {
|
public func wantsDisplayBelowKeyboard() -> Bool {
|
||||||
if let emojiView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
|
if let emojiView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
|
||||||
return emojiView.wantsDisplayBelowKeyboard()
|
return emojiView.wantsDisplayBelowKeyboard()
|
||||||
|
} else if let stickersView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("stickers"))) as? EmojiPagerContentComponent.View {
|
||||||
|
return stickersView.wantsDisplayBelowKeyboard()
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1047,8 +1051,16 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
itemTransition = .immediate
|
itemTransition = .immediate
|
||||||
|
|
||||||
switch self.items[i] {
|
switch self.items[i] {
|
||||||
case let .reaction(item):
|
case let .reaction(item, icon):
|
||||||
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: self.reactionsLocked)
|
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
|
maskNode = nil
|
||||||
case let .staticEmoji(emoji):
|
case let .staticEmoji(emoji):
|
||||||
itemNode = EmojiItemNode(theme: self.presentationData.theme, emoji: emoji)
|
itemNode = EmojiItemNode(theme: self.presentationData.theme, emoji: emoji)
|
||||||
@ -1498,6 +1510,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
animationOffsetY += 54.0
|
animationOffsetY += 54.0
|
||||||
} else if self.alwaysAllowPremiumReactions {
|
} else if self.alwaysAllowPremiumReactions {
|
||||||
animationOffsetY += 4.0
|
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 {
|
} else {
|
||||||
animationOffsetY += 46.0 + 54.0 - 4.0
|
animationOffsetY += 46.0 + 54.0 - 4.0
|
||||||
}
|
}
|
||||||
@ -1814,115 +1829,147 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|
|
||||||
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then(
|
let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError>
|
||||||
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
|
if self.isMessageEffects {
|
||||||
($0, true)
|
resultSignal = signal
|
||||||
}
|
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||||
)
|
var allEmoticons: [String: String] = [:]
|
||||||
|
for keyword in keywords {
|
||||||
let resultSignal = signal
|
for emoticon in keyword.emoticons {
|
||||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
allEmoticons[emoticon] = keyword.keyword
|
||||||
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(
|
return context.availableMessageEffects
|
||||||
supergroupId: "search",
|
|> take(1)
|
||||||
groupId: "search",
|
|> mapToSignal { availableMessageEffects -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||||
title: nil,
|
guard let availableMessageEffects else {
|
||||||
subtitle: nil,
|
return .single([])
|
||||||
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)] = []
|
|
||||||
|
|
||||||
var allEmoticons: [String: String] = [:]
|
var filteredEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||||
for keyword in keywords {
|
for messageEffect in availableMessageEffects.messageEffects {
|
||||||
for emoticon in keyword.emoticons {
|
if allEmoticons[messageEffect.emoticon] != nil {
|
||||||
allEmoticons[emoticon] = keyword.keyword
|
filteredEffects.append(messageEffect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in view.entries {
|
var reactionEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||||
guard let item = entry.item as? StickerPackItem else {
|
var stickerEffects: [AvailableMessageEffects.MessageEffect] = []
|
||||||
continue
|
for messageEffect in filteredEffects {
|
||||||
}
|
if messageEffect.effectAnimation != nil {
|
||||||
for attribute in item.file.attributes {
|
reactionEffects.append(messageEffect)
|
||||||
switch attribute {
|
} else {
|
||||||
case let .CustomEmoji(_, _, alt, _):
|
stickerEffects.append(messageEffect)
|
||||||
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 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>()
|
var resultGroups: [ItemGroup] = []
|
||||||
for item in result {
|
var resultGroupIndexById: [AnyHashable: Int] = [:]
|
||||||
if let itemFile = item.1 {
|
|
||||||
if existingIds.contains(itemFile.fileId) {
|
for i in 0 ..< 2 {
|
||||||
continue
|
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 animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
|
||||||
let item = EmojiPagerContentComponent.Item(
|
let resultItem = EmojiPagerContentComponent.Item(
|
||||||
animationData: animationData,
|
animationData: animationData,
|
||||||
content: .animation(animationData),
|
content: .animation(animationData),
|
||||||
itemFile: itemFile, subgroupId: nil,
|
itemFile: itemFile,
|
||||||
|
subgroupId: nil,
|
||||||
icon: .none,
|
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] = []
|
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
||||||
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||||
supergroupId: "search",
|
supergroupId: "search",
|
||||||
@ -1942,59 +1989,138 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
fillWithLoadingPlaceholders: false,
|
fillWithLoadingPlaceholders: false,
|
||||||
items: items
|
items: items
|
||||||
))
|
))
|
||||||
|
return .single(resultGroups)
|
||||||
for (collectionId, info, _, _) in foundPacks.sets.infos {
|
} else {
|
||||||
if let info = info as? StickerPackCollectionInfo {
|
return combineLatest(
|
||||||
var topItems: [StickerPackItem] = []
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
||||||
for e in foundPacks.sets.entries {
|
context.engine.stickers.availableReactions() |> take(1),
|
||||||
if let item = e.item as? StickerPackItem {
|
hasPremium |> take(1),
|
||||||
if e.index.collectionId == collectionId {
|
remotePacksSignal
|
||||||
topItems.append(item)
|
)
|
||||||
|
|> 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 items: [EmojiPagerContentComponent.Item] = []
|
||||||
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
|
||||||
if item.file.isCustomTemplateEmoji {
|
var existingIds = Set<MediaId>()
|
||||||
tintMode = .primary
|
for item in result {
|
||||||
|
if let itemFile = item.1 {
|
||||||
|
if existingIds.contains(itemFile.fileId) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
existingIds.insert(itemFile.fileId)
|
||||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||||
let resultItem = EmojiPagerContentComponent.Item(
|
let item = EmojiPagerContentComponent.Item(
|
||||||
animationData: animationData,
|
animationData: animationData,
|
||||||
content: .animation(animationData),
|
content: .animation(animationData),
|
||||||
itemFile: item.file,
|
itemFile: itemFile, subgroupId: nil,
|
||||||
subgroupId: nil,
|
|
||||||
icon: .none,
|
icon: .none,
|
||||||
tintMode: tintMode
|
tintMode: animationData.isTemplate ? .primary : .none
|
||||||
)
|
)
|
||||||
|
items.append(item)
|
||||||
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
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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):
|
case let .category(value):
|
||||||
let resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|
let context = self.context
|
||||||
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError>
|
||||||
var items: [EmojiPagerContentComponent.Item] = []
|
if self.isMessageEffects {
|
||||||
|
let keywords: Signal<[String], NoError> = .single(value.identifiers)
|
||||||
var existingIds = Set<MediaId>()
|
resultSignal = keywords
|
||||||
for itemFile in files {
|
|> mapToSignal { keywords -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||||
if existingIds.contains(itemFile.fileId) {
|
var allEmoticons: [String: String] = [:]
|
||||||
continue
|
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)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|
||||||
supergroupId: "search",
|
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||||
groupId: "search",
|
var items: [EmojiPagerContentComponent.Item] = []
|
||||||
title: nil,
|
|
||||||
subtitle: nil,
|
var existingIds = Set<MediaId>()
|
||||||
badge: nil,
|
for itemFile in files {
|
||||||
actionButtonTitle: nil,
|
if existingIds.contains(itemFile.fileId) {
|
||||||
isFeatured: false,
|
continue
|
||||||
isPremiumLocked: false,
|
}
|
||||||
isEmbedded: false,
|
existingIds.insert(itemFile.fileId)
|
||||||
hasClear: false,
|
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||||
hasEdit: false,
|
let item = EmojiPagerContentComponent.Item(
|
||||||
collapsedLineCount: nil,
|
animationData: animationData,
|
||||||
displayPremiumBadges: false,
|
content: .animation(animationData),
|
||||||
headerItem: nil,
|
itemFile: itemFile, subgroupId: nil,
|
||||||
fillWithLoadingPlaceholders: false,
|
icon: .none,
|
||||||
items: items
|
tintMode: animationData.isTemplate ? .primary : .none
|
||||||
)], isFinalResult))
|
)
|
||||||
|
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
|
var version = 0
|
||||||
@ -2101,7 +2338,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
peekBehavior: nil,
|
peekBehavior: nil,
|
||||||
customLayout: emojiContentLayout,
|
customLayout: emojiContentLayout,
|
||||||
externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground(
|
externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground(
|
||||||
effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView
|
effectContainerView: self.backgroundNode.vibrantExpandedContentContainer
|
||||||
),
|
),
|
||||||
externalExpansionView: self.view,
|
externalExpansionView: self.view,
|
||||||
customContentView: nil,
|
customContentView: nil,
|
||||||
@ -2310,7 +2547,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let customReactionSource = self.customReactionSource {
|
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 {
|
if let contents = customReactionSource.layer.contents {
|
||||||
itemNode.setCustomContents(contents: contents)
|
itemNode.setCustomContents(contents: contents)
|
||||||
}
|
}
|
||||||
@ -2737,11 +2974,13 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring))
|
self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring))
|
||||||
} else if let reaction = self.reaction(at: point) {
|
} else if let reaction = self.reaction(at: point) {
|
||||||
switch reaction {
|
switch reaction {
|
||||||
case let .reaction(reactionItem):
|
case let .reaction(reactionItem, icon):
|
||||||
if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable {
|
if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable {
|
||||||
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||||
} else if self.reactionsLocked {
|
} else if self.reactionsLocked {
|
||||||
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||||
|
} else if case .locked = icon {
|
||||||
|
self.premiumReactionsSelected?(reactionItem.stillAnimation)
|
||||||
} else {
|
} else {
|
||||||
self.reactionSelected?(reactionItem.updateMessageReaction, false)
|
self.reactionSelected?(reactionItem.updateMessageReaction, false)
|
||||||
}
|
}
|
||||||
@ -2915,7 +3154,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||||||
if !itemNode.isAnimationLoaded {
|
if !itemNode.isAnimationLoaded {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return .reaction(itemNode.item)
|
return .reaction(item: itemNode.item, icon: itemNode.icon)
|
||||||
} else if let itemNode = itemNode as? EmojiItemNode {
|
} else if let itemNode = itemNode as? EmojiItemNode {
|
||||||
return .staticEmoji(itemNode.emoji)
|
return .staticEmoji(itemNode.emoji)
|
||||||
} else if let _ = itemNode as? PremiumReactionsNode {
|
} else if let _ = itemNode as? PremiumReactionsNode {
|
||||||
@ -2999,7 +3238,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
itemNode = currentItemNode
|
itemNode = currentItemNode
|
||||||
} else {
|
} else {
|
||||||
let animationRenderer = MultiAnimationRendererImpl()
|
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
|
self.itemNode = itemNode
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,6 +14,7 @@ import AnimationCache
|
|||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
import ShimmerEffect
|
import ShimmerEffect
|
||||||
import GenerateStickerPlaceholderImage
|
import GenerateStickerPlaceholderImage
|
||||||
|
import EntityKeyboard
|
||||||
|
|
||||||
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
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
|
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)
|
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)
|
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
|
||||||
|
|
||||||
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let item: ReactionItem
|
let item: ReactionItem
|
||||||
|
let icon: EmojiPagerContentComponent.Item.Icon
|
||||||
private let loopIdle: Bool
|
private let loopIdle: Bool
|
||||||
private let isLocked: Bool
|
private let isLocked: Bool
|
||||||
private let hasAppearAnimation: Bool
|
private let hasAppearAnimation: Bool
|
||||||
@ -102,10 +104,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
|||||||
return self.staticAnimationNode.currentFrameImage != nil
|
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.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.item = item
|
self.item = item
|
||||||
|
self.icon = icon
|
||||||
self.loopIdle = loopIdle
|
self.loopIdle = loopIdle
|
||||||
self.isLocked = isLocked
|
self.isLocked = isLocked
|
||||||
self.hasAppearAnimation = hasAppearAnimation
|
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 {
|
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))
|
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)
|
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))
|
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))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let isPremium = (flags & (1 << 3)) != 0
|
let isPremium = (flags & (1 << 2)) != 0
|
||||||
self.init(
|
self.init(
|
||||||
id: id,
|
id: id,
|
||||||
isPremium: isPremium,
|
isPremium: isPremium,
|
||||||
@ -239,7 +239,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals: [Signal<Never, NoError>] = []
|
/*var signals: [Signal<Never, NoError>] = []
|
||||||
|
|
||||||
if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) {
|
if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) {
|
||||||
var resources: [MediaResource] = []
|
var resources: [MediaResource] = []
|
||||||
@ -271,7 +271,9 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
|
|||||||
}
|
}
|
||||||
|
|
||||||
return combineLatest(signals)
|
return combineLatest(signals)
|
||||||
|> ignoreValues
|
|> ignoreValues*/
|
||||||
|
|
||||||
|
return .complete()
|
||||||
}
|
}
|
||||||
|> switchToLatest
|
|> switchToLatest
|
||||||
})
|
})
|
||||||
|
@ -312,6 +312,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var currentRightInset: CGFloat {
|
||||||
|
return self.customTextContainer.rightInset
|
||||||
|
}
|
||||||
|
|
||||||
private var didInitializePrimaryInputLanguage: Bool = false
|
private var didInitializePrimaryInputLanguage: Bool = false
|
||||||
public var initialPrimaryLanguage: String?
|
public var initialPrimaryLanguage: String?
|
||||||
|
|
||||||
@ -656,6 +660,45 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
|||||||
self.isUpdatingLayout = false
|
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() {
|
public func updateTextElements() {
|
||||||
var blockQuoteIndex = 0
|
var blockQuoteIndex = 0
|
||||||
var validBlockQuotes: [Int] = []
|
var validBlockQuotes: [Int] = []
|
||||||
|
@ -633,6 +633,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.visibilityStatus = self.visibility != .none
|
self.visibilityStatus = self.visibility != .none
|
||||||
|
|
||||||
|
self.updateVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -648,8 +650,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
containerSize: credibilityIconView.bounds.size
|
containerSize: credibilityIconView.bounds.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateVisibility()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5840,7 +5840,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
do {
|
do {
|
||||||
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
|
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.updateLayout(size: animationSize)
|
||||||
additionalAnimationNode.setup(source: source, width: Int(animationSize.width), height: Int(animationSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
additionalAnimationNode.setup(source: source, width: Int(animationSize.width), height: Int(animationSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
||||||
var animationFrame: CGRect
|
var animationFrame: CGRect
|
||||||
@ -5925,32 +5930,46 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
return
|
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 {
|
if !isPlaying {
|
||||||
self.removeAdditionalAnimations()
|
self.removeAdditionalAnimations()
|
||||||
}
|
}
|
||||||
|
|
||||||
var alreadySeen = true
|
if isPlaying {
|
||||||
if item.message.flags.contains(.Incoming) {
|
var alreadySeen = true
|
||||||
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
|
if item.message.flags.contains(.Incoming) {
|
||||||
if unreadRange.contains(item.message.id.id) {
|
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) {
|
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||||
alreadySeen = false
|
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,
|
context: context,
|
||||||
animationCache: context.animationCache,
|
animationCache: context.animationCache,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
items: reactionItems.map(ReactionContextItem.reaction),
|
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||||
selectedItems: actions.editTags,
|
selectedItems: actions.editTags,
|
||||||
title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag,
|
title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag,
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
|
@ -31,7 +31,7 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio
|
|||||||
context: context,
|
context: context,
|
||||||
animationCache: context.animationCache,
|
animationCache: context.animationCache,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
items: reactionItems.map(ReactionContextItem.reaction),
|
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
|
||||||
selectedItems: Set(),
|
selectedItems: Set(),
|
||||||
title: isSingleMessage ? presentationData.strings.Chat_ForwardToSavedMessageTagSelectionTitle : presentationData.strings.Chat_ForwardToSavedMessagesTagSelectionTitle,
|
title: isSingleMessage ? presentationData.strings.Chat_ForwardToSavedMessageTagSelectionTitle : presentationData.strings.Chat_ForwardToSavedMessagesTagSelectionTitle,
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
|
@ -47,6 +47,7 @@ swift_library(
|
|||||||
"//submodules/rlottie:RLottieBinding",
|
"//submodules/rlottie:RLottieBinding",
|
||||||
"//submodules/lottie-ios:Lottie",
|
"//submodules/lottie-ios:Lottie",
|
||||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||||
|
"//submodules/TelegramUIPreferences",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//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 final class PremiumBadgeView: UIView {
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
|
private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
|
||||||
|
|
||||||
let contentLayer: SimpleLayer
|
let contentLayer: SimpleLayer
|
||||||
private let overlayColorLayer: SimpleLayer
|
private let overlayColorLayer: SimpleLayer
|
||||||
private let iconLayer: SimpleLayer
|
private let iconLayer: SimpleLayer
|
||||||
|
private var customFileLayer: InlineFileIconLayer?
|
||||||
|
|
||||||
init() {
|
init(context: AccountContext) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
self.contentLayer = SimpleLayer()
|
self.contentLayer = SimpleLayer()
|
||||||
self.contentLayer.contentsGravity = .resize
|
self.contentLayer.contentsGravity = .resize
|
||||||
self.contentLayer.masksToBounds = true
|
self.contentLayer.masksToBounds = true
|
||||||
@ -641,6 +650,47 @@ private final class PremiumBadgeView: UIView {
|
|||||||
self.iconLayer.contents = featuredBadgeIcon?.cgImage
|
self.iconLayer.contents = featuredBadgeIcon?.cgImage
|
||||||
case .locked:
|
case .locked:
|
||||||
self.iconLayer.contents = lockedBadgeIcon?.cgImage
|
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
|
iconInset = 0.0
|
||||||
case .locked:
|
case .locked:
|
||||||
iconInset = 0.0
|
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
|
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.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))
|
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 none
|
||||||
case locked
|
case locked
|
||||||
case premium
|
case premium
|
||||||
|
case text(String)
|
||||||
|
case customFile(TelegramMediaFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TintMode: Equatable {
|
public enum TintMode: Equatable {
|
||||||
@ -3448,13 +3516,16 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Badge {
|
enum Badge: Equatable {
|
||||||
case premium
|
case premium
|
||||||
case locked
|
case locked
|
||||||
case featured
|
case featured
|
||||||
|
case text(String)
|
||||||
|
case customFile(TelegramMediaFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let item: Item
|
public let item: Item
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
private var content: ItemContent
|
private var content: ItemContent
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
@ -3566,6 +3637,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
||||||
) {
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
|
self.context = context
|
||||||
self.content = content
|
self.content = content
|
||||||
self.placeholderColor = placeholderColor
|
self.placeholderColor = placeholderColor
|
||||||
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||||
@ -3717,6 +3789,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.context = layer.context
|
||||||
self.item = layer.item
|
self.item = layer.item
|
||||||
|
|
||||||
self.content = layer.content
|
self.content = layer.content
|
||||||
@ -3837,7 +3910,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
premiumBadgeView = current
|
premiumBadgeView = current
|
||||||
} else {
|
} else {
|
||||||
badgeTransition = .immediate
|
badgeTransition = .immediate
|
||||||
premiumBadgeView = PremiumBadgeView()
|
premiumBadgeView = PremiumBadgeView(context: self.context)
|
||||||
self.premiumBadgeView = premiumBadgeView
|
self.premiumBadgeView = premiumBadgeView
|
||||||
self.addSublayer(premiumBadgeView.layer)
|
self.addSublayer(premiumBadgeView.layer)
|
||||||
}
|
}
|
||||||
@ -6202,6 +6275,10 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
badge = .locked
|
badge = .locked
|
||||||
case .premium:
|
case .premium:
|
||||||
badge = .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 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(
|
return combineLatest(
|
||||||
hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false),
|
hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false),
|
||||||
@ -2225,13 +2225,30 @@ public extension EmojiPagerContentComponent {
|
|||||||
tintMode = .primary
|
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 animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
|
||||||
let resultItem = EmojiPagerContentComponent.Item(
|
let resultItem = EmojiPagerContentComponent.Item(
|
||||||
animationData: animationData,
|
animationData: animationData,
|
||||||
content: .animation(animationData),
|
content: .animation(animationData),
|
||||||
itemFile: itemFile,
|
itemFile: itemFile,
|
||||||
subgroupId: nil,
|
subgroupId: nil,
|
||||||
icon: .none,
|
icon: icon,
|
||||||
tintMode: tintMode
|
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 = [
|
copts = [
|
||||||
"-Werror",
|
"-Werror",
|
||||||
"-I{}/Sources".format(package_name()),
|
"-I{}/Sources".format(package_name()),
|
||||||
"-O2",
|
|
||||||
],
|
],
|
||||||
hdrs = glob([
|
hdrs = glob([
|
||||||
"PublicHeaders/**/*.h",
|
"PublicHeaders/**/*.h",
|
||||||
@ -32,3 +31,18 @@ objc_library(
|
|||||||
"//visibility:public",
|
"//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" {
|
extern "C" {
|
||||||
#endif
|
#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
|
@interface LottieAnimationContainer : NSObject
|
||||||
|
|
||||||
@property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation;
|
@property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation;
|
||||||
@ -34,6 +58,10 @@ extern "C" {
|
|||||||
- (std::shared_ptr<lottie::RenderTreeNode>)internalGetRootRenderTreeNode;
|
- (std::shared_ptr<lottie::RenderTreeNode>)internalGetRootRenderTreeNode;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
- (int64_t)getRootRenderNodeProxy;
|
||||||
|
- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct));
|
||||||
|
- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct));
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -10,19 +10,10 @@ void batchInterpolate(std::vector<PathElement> const &from, std::vector<PathElem
|
|||||||
elementCount = (int)to.size();
|
elementCount = (int)to.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizeof(PathElement) == 8 * 2 * 3) {
|
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);
|
resultPath.setElementCount(elementCount);
|
||||||
} else {
|
vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -178,15 +178,6 @@ public:
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
batchInterpolate(value.elements(), to.elements(), resultPath, amount);
|
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 "LottieAnimationInternal.h"
|
||||||
#include "RenderNode.hpp"
|
#include "RenderNode.hpp"
|
||||||
#include "LottieRenderTreeInternal.h"
|
#include "LottieRenderTreeInternal.h"
|
||||||
|
#include <LottieCpp/VectorsCocoa.h>
|
||||||
|
|
||||||
namespace lottie {
|
namespace lottie {
|
||||||
|
|
||||||
@ -367,6 +368,48 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
|
|||||||
return renderNode;
|
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
|
@end
|
||||||
|
|
||||||
@implementation LottieAnimationContainer (Internal)
|
@implementation LottieAnimationContainer (Internal)
|
||||||
|
@ -52,6 +52,59 @@ private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount:
|
|||||||
return device.makeTexture(descriptor: textureDescriptor)!
|
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 {
|
private final class AnimationCacheState {
|
||||||
static let shared = AnimationCacheState()
|
static let shared = AnimationCacheState()
|
||||||
|
|
||||||
@ -118,45 +171,8 @@ private final class AnimationCacheState {
|
|||||||
let cachePath = task.cachePath
|
let cachePath = task.cachePath
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
Queue.concurrentDefaultQueue().async { [weak self, weak task] in
|
Queue.concurrentDefaultQueue().async { [weak self, weak task] in
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
if let zippedData = cacheLottieMetalAnimation(path: path) {
|
||||||
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
|
let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.async {
|
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 {
|
public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||||
enum Content {
|
public enum Content {
|
||||||
case serialized(frameMapping: SerializedFrameMapping, data: Data)
|
case serialized(frameMapping: SerializedLottieMetalFrameMapping, data: Data)
|
||||||
case animation(LottieAnimationContainer)
|
case animation(LottieAnimationContainer)
|
||||||
|
|
||||||
var size: CGSize {
|
public var size: CGSize {
|
||||||
switch self {
|
switch self {
|
||||||
case let .serialized(frameMapping, _):
|
case let .serialized(frameMapping, _):
|
||||||
return frameMapping.size
|
return frameMapping.size
|
||||||
@ -205,7 +580,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var frameCount: Int {
|
public var frameCount: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case let .serialized(frameMapping, _):
|
case let .serialized(frameMapping, _):
|
||||||
return frameMapping.frameCount
|
return frameMapping.frameCount
|
||||||
@ -214,7 +589,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var framesPerSecond: Int {
|
public var framesPerSecond: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case let .serialized(frameMapping, _):
|
case let .serialized(frameMapping, _):
|
||||||
return frameMapping.framesPerSecond
|
return frameMapping.framesPerSecond
|
||||||
@ -288,7 +663,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(content: Content) {
|
public init(content: Content) {
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -312,71 +687,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
|
private var renderNodeCache: [Int: LottieRenderNode] = [:]
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func update(context: MetalEngineSubjectContext) {
|
public func update(context: MetalEngineSubjectContext) {
|
||||||
if self.bounds.isEmpty {
|
if self.bounds.isEmpty {
|
||||||
@ -392,196 +703,32 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
return
|
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
|
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.currentBuffer.reset()
|
||||||
self.currentBezierIndicesBuffer.reset()
|
self.currentBezierIndicesBuffer.reset()
|
||||||
let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: self.msaaSampleCount, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer)
|
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 {
|
final class ComputeOutput {
|
||||||
let pathRenderContext: PathRenderContext
|
let pathRenderContext: PathRenderContext
|
||||||
@ -693,7 +840,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
self.offscreenHeap = offscreenHeap
|
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 {
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||||
self.multisampleTextureQueue.append(multisampleTexture)
|
self.multisampleTextureQueue.append(multisampleTexture)
|
||||||
@ -701,7 +848,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize)
|
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: frameContext.canvasSize)
|
||||||
|
|
||||||
renderEncoder.endEncoding()
|
renderEncoder.endEncoding()
|
||||||
|
|
||||||
@ -866,15 +1013,15 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var serializedFrames: (SerializedFrameMapping, Data)?
|
var serializedFrames: (SerializedLottieMetalFrameMapping, Data)?
|
||||||
var cachePathValue: String?
|
var cachePathValue: String?
|
||||||
if let cachePathPrefix {
|
if let cachePathPrefix {
|
||||||
let cachePath = cachePathPrefix + "-metal1"
|
let cachePath = cachePathPrefix + "-metal1"
|
||||||
cachePathValue = cachePath
|
cachePathValue = cachePath
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: cachePath), options: .mappedIfSafe) {
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: cachePath), options: .mappedIfSafe) {
|
||||||
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
|
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
|
||||||
let serializedFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
|
let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
|
||||||
serializedFrames = (serializedFrameMapping, unzippedData)
|
serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -484,14 +484,14 @@ func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SerializedFrameMapping {
|
public struct SerializedLottieMetalFrameMapping {
|
||||||
var size: CGSize = CGSize()
|
var size: CGSize = CGSize()
|
||||||
var frameCount: Int = 0
|
var frameCount: Int = 0
|
||||||
var framesPerSecond: Int = 0
|
var framesPerSecond: Int = 0
|
||||||
var frameRanges: [Int: Range<Int>] = [:]
|
var frameRanges: [Int: Range<Int>] = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMapping) {
|
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedLottieMetalFrameMapping) {
|
||||||
buffer.write(size: frameMapping.size)
|
buffer.write(size: frameMapping.size)
|
||||||
buffer.write(uInt32: UInt32(frameMapping.frameCount))
|
buffer.write(uInt32: UInt32(frameMapping.frameCount))
|
||||||
buffer.write(uInt32: UInt32(frameMapping.framesPerSecond))
|
buffer.write(uInt32: UInt32(frameMapping.framesPerSecond))
|
||||||
@ -502,8 +502,8 @@ func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedFrameMapping {
|
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedLottieMetalFrameMapping {
|
||||||
var frameMapping = SerializedFrameMapping()
|
var frameMapping = SerializedLottieMetalFrameMapping()
|
||||||
|
|
||||||
frameMapping.size = buffer.readSize()
|
frameMapping.size = buffer.readSize()
|
||||||
frameMapping.frameCount = Int(buffer.readUInt32())
|
frameMapping.frameCount = Int(buffer.readUInt32())
|
||||||
|
@ -4450,7 +4450,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
animationCache: component.context.animationCache,
|
animationCache: component.context.animationCache,
|
||||||
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme),
|
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(),
|
selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
|
||||||
title: self.displayLikeReactions ? nil : (isGroup ? component.strings.Story_SendReactionAsGroupMessage : component.strings.Story_SendReactionAsMessage),
|
title: self.displayLikeReactions ? nil : (isGroup ? component.strings.Story_SendReactionAsGroupMessage : component.strings.Story_SendReactionAsMessage),
|
||||||
reactionsLocked: false,
|
reactionsLocked: false,
|
||||||
|
@ -111,7 +111,7 @@ extension ChatControllerImpl {
|
|||||||
actions.animationCache = self.controllerInteraction?.presentationContext.animationCache
|
actions.animationCache = self.controllerInteraction?.presentationContext.animationCache
|
||||||
|
|
||||||
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
|
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
|
actions.selectedReactionItems = selectedReactions.reactions
|
||||||
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
|
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
|
||||||
if self.presentationInterfaceState.isPremium {
|
if self.presentationInterfaceState.isPremium {
|
||||||
@ -131,7 +131,7 @@ extension ChatControllerImpl {
|
|||||||
if !actions.reactionItems.isEmpty {
|
if !actions.reactionItems.isEmpty {
|
||||||
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
|
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
|
||||||
switch item {
|
switch item {
|
||||||
case let .reaction(reaction):
|
case let .reaction(reaction, _):
|
||||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
@ -45,11 +45,22 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
|||||||
effectItems = .single(nil)
|
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(
|
let _ = (combineLatest(
|
||||||
selfController.context.account.viewTracker.peerView(peerId) |> take(1),
|
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 {
|
guard let selfController, let peer = peerViewMainPeer(peerView) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,7 +109,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
selfController.controllerInteraction?.scheduleCurrentMessage()
|
selfController.controllerInteraction?.scheduleCurrentMessage()
|
||||||
}, reactionItems: effectItems)
|
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
|
||||||
selfController.sendMessageActionsController = controller
|
selfController.sendMessageActionsController = controller
|
||||||
if layout.isNonExclusive {
|
if layout.isNonExclusive {
|
||||||
selfController.present(controller, in: .window(.root))
|
selfController.present(controller, in: .window(.root))
|
||||||
|
@ -59,6 +59,8 @@ public protocol WallpaperBubbleBackgroundNode: ASDisplayNode {
|
|||||||
func update(rect: CGRect, within containerSize: CGSize, animator: ControlledTransitionAnimator)
|
func update(rect: CGRect, within containerSize: CGSize, animator: ControlledTransitionAnimator)
|
||||||
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double)
|
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double)
|
||||||
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat)
|
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat)
|
||||||
|
|
||||||
|
func reloadBindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WallpaperDisplayMode {
|
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 {
|
final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
|
||||||
var implicitContentUpdate: Bool = true
|
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)
|
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 {
|
final class BubbleBackgroundPortalNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
|
||||||
@ -679,6 +684,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
|
|
||||||
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reloadBindings() {
|
||||||
|
self.portalView.reloadPortal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class BubbleBackgroundNodeReference {
|
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 {
|
didSet {
|
||||||
var fromValue: CGFloat = 0.0
|
var fromValue: CGFloat = 0.0
|
||||||
if let value = (self.layer.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue {
|
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 static var cachedSharedPattern: (PatternKey, UIImage)?
|
||||||
|
|
||||||
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
var isReady: Signal<Bool, NoError> {
|
public var isReady: Signal<Bool, NoError> {
|
||||||
return self._isReady.get()
|
return self._isReady.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -920,7 +929,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
self.dimLayer.opacity = dimAlpha
|
self.dimLayer.opacity = dimAlpha
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(wallpaper: TelegramWallpaper, animated: Bool) {
|
public func update(wallpaper: TelegramWallpaper, animated: Bool) {
|
||||||
if self.wallpaper == wallpaper {
|
if self.wallpaper == wallpaper {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1074,7 +1083,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
self.updateDimming()
|
self.updateDimming()
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internalUpdateIsSettingUpWallpaper() {
|
public func _internalUpdateIsSettingUpWallpaper() {
|
||||||
self.isSettingUpWallpaper = true
|
self.isSettingUpWallpaper = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1301,7 +1310,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size))
|
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
|
let isFirstLayout = self.validLayout == nil
|
||||||
self.validLayout = (size, displayMode)
|
self.validLayout = (size, displayMode)
|
||||||
|
|
||||||
@ -1357,7 +1366,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
private var isAnimating = false
|
private var isAnimating = false
|
||||||
private var isLooping = false
|
private var isLooping = false
|
||||||
|
|
||||||
func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
|
public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
|
||||||
guard !(self.isLooping && self.isAnimating) else {
|
guard !(self.isLooping && self.isAnimating) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1373,7 +1382,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {})
|
self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateIsLooping(_ isLooping: Bool) {
|
public func updateIsLooping(_ isLooping: Bool) {
|
||||||
let wasLooping = self.isLooping
|
let wasLooping = self.isLooping
|
||||||
self.isLooping = 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 {
|
if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners {
|
||||||
self.bubbleTheme = bubbleTheme
|
self.bubbleTheme = bubbleTheme
|
||||||
self.bubbleCorners = bubbleCorners
|
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 {
|
guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1474,13 +1483,18 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
|
|
||||||
return false
|
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) {
|
if !self.hasBubbleBackground(for: type) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
#if true
|
|
||||||
var sourceView: PortalSourceView?
|
var sourceView: PortalSourceView?
|
||||||
switch type {
|
switch type {
|
||||||
case .free:
|
case .free:
|
||||||
@ -1499,14 +1513,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
|
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
|
||||||
return node
|
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) {
|
if !self.hasBubbleBackground(for: .free) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1519,7 +1528,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasExtraBubbleBackground() -> Bool {
|
public func hasExtraBubbleBackground() -> Bool {
|
||||||
var isInvertedGradient = false
|
var isInvertedGradient = false
|
||||||
switch self.wallpaper {
|
switch self.wallpaper {
|
||||||
case let .file(file):
|
case let .file(file):
|
||||||
@ -1532,7 +1541,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
|
|||||||
return isInvertedGradient
|
return isInvertedGradient
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeDimmedNode() -> ASDisplayNode? {
|
public func makeDimmedNode() -> ASDisplayNode? {
|
||||||
if let gradientBackgroundNode = self.gradientBackgroundNode {
|
if let gradientBackgroundNode = self.gradientBackgroundNode {
|
||||||
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
|
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user