[WIP] Message effects

This commit is contained in:
Isaac 2024-05-10 20:57:12 +04:00
parent fe8c2d8c15
commit 7ef63a81df
43 changed files with 3050 additions and 704 deletions

2
Tests/LottieMetalMacTest/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
TestData/*.json

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

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

View File

@ -0,0 +1,11 @@
import Cocoa
@objc(ViewController)
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.layer?.backgroundColor = NSColor.blue.cgColor
}
}

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

View File

@ -161,6 +161,7 @@ func buildAnimationFolderItems(basePath: String, path: String) -> [(String, Stri
return result
}
@available (iOS 13.0, *)
private func processAnimationFolderItems(items: [(String, String)], countPerBucket: Int, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
let bucketCount = items.count / countPerBucket
var buckets: [[(String, String)]] = []
@ -253,6 +254,7 @@ private func processAnimationFolderItemsParallel(items: [(String, String)], stop
return result
}
@available (iOS 13.0, *)
func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
let items = buildAnimationFolderItems(basePath: basePath, path: path)
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)

View File

@ -110,6 +110,8 @@ public final class ViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
SharedDisplayLinkDriver.shared.updateForegroundState(true)
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
let filePath = bundlePath + "/fireworks.json"
@ -117,12 +119,15 @@ public final class ViewController: UIViewController {
self.view.layer.addSublayer(MetalEngine.shared.rootLayer)
if "".isEmpty {
if !"".isEmpty {
if #available(iOS 13.0, *) {
self.test = ReferenceCompareTest(view: self.view)
}
} else if !"".isEmpty {
let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
} else if "".isEmpty {
let cachedAnimation = cacheLottieMetalAnimation(path: filePath)!
let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)!
/*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath))
var startTime = CFAbsoluteTimeGetCurrent()
let animation = LottieAnimation(data: animationData)!
@ -131,9 +136,9 @@ public final class ViewController: UIViewController {
startTime = CFAbsoluteTimeGetCurrent()
let animationContainer = LottieAnimationContainer(animation: animation)
animationContainer.update(0)
print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")*/
let lottieLayer = LottieContentLayer(animation: animationContainer)
let lottieLayer = LottieContentLayer(content: animation)
lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0))
self.view.layer.addSublayer(lottieLayer)
lottieLayer.setNeedsUpdate()
@ -162,7 +167,7 @@ public final class ViewController: UIViewController {
var frameIndex = 0
while true {
animationContainer.update(frameIndex)
let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false)
//let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false)
frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount
numUpdates += 1
let timestamp = CFAbsoluteTimeGetCurrent()

View File

@ -168,7 +168,7 @@ public protocol AnimatedStickerNode: ASDisplayNode {
var visibility: Bool { get set }
var overrideVisibility: Bool { get set }
var isPlayingChanged: (Bool) -> Void { get }
var isPlayingChanged: (Bool) -> Void { get set }
func cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)

View File

@ -39,6 +39,8 @@ swift_library(
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
],
visibility = [
"//visibility:public",

View File

@ -21,6 +21,8 @@ import ShimmerEffect
import TextFormat
import LegacyMessageInputPanel
import LegacyMessageInputPanelInputView
import ReactionSelectionNode
import TopMessageReactions
private let buttonSize = CGSize(width: 88.0, height: 49.0)
private let smallButtonWidth: CGFloat = 69.0
@ -926,9 +928,31 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
if case .media = strongSelf.presentationInterfaceState.inputMode {
hasEntityKeyboard = true
}
let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
let effectItems: Signal<[ReactionItem]?, NoError>
if strongSelf.presentationInterfaceState.chatLocation.peerId != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser {
effectItems = effectMessageReactions(context: strongSelf.context)
|> map(Optional.init)
} else {
effectItems = .single(nil)
}
let availableMessageEffects = strongSelf.context.availableMessageEffects |> take(1)
let hasPremium = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
let _ = (combineLatest(
strongSelf.context.account.viewTracker.peerView(peerId) |> take(1),
effectItems,
availableMessageEffects,
hasPremium
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView, effectItems, availableMessageEffects, hasPremium in
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
return
}
@ -955,7 +979,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}
}, schedule: { [weak textInputPanelNode] _ in
textInputPanelNode?.sendMessage(.schedule)
})
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
strongSelf.presentInGlobalOverlay(controller)
})
}, openScheduledMessages: {

View File

@ -492,7 +492,7 @@ public func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: Princ
}
public final class ChatMessageBubbleBackdrop: ASDisplayNode {
private var backgroundContent: WallpaperBubbleBackgroundNode?
public private(set) var backgroundContent: WallpaperBubbleBackgroundNode?
private var currentType: ChatMessageBackgroundType?
private var currentMaskMode: Bool?

View File

@ -33,7 +33,10 @@ swift_library(
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/LottieMetal",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ActivityIndicator",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",

View File

@ -210,7 +210,9 @@ public func makeChatSendMessageActionSheetController(
completion: @escaping () -> Void,
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
reactionItems: [ReactionItem]? = nil
reactionItems: [ReactionItem]? = nil,
availableMessageEffects: AvailableMessageEffects? = nil,
isPremium: Bool = false
) -> ChatSendMessageActionSheetController {
if textInputView.text.isEmpty {
return ChatSendMessageActionSheetControllerImpl(
@ -229,7 +231,7 @@ public func makeChatSendMessageActionSheetController(
completion: completion,
sendMessage: sendMessage,
schedule: schedule,
reactionItems: reactionItems
reactionItems: nil
)
}
@ -250,6 +252,8 @@ public func makeChatSendMessageActionSheetController(
completion: completion,
sendMessage: sendMessage,
schedule: schedule,
reactionItems: reactionItems
reactionItems: reactionItems,
availableMessageEffects: availableMessageEffects,
isPremium: isPremium
)
}

View File

@ -388,7 +388,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
context: context,
animationCache: context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: Set(),
title: "Add an animated effect",
reactionsLocked: false,

View File

@ -18,6 +18,9 @@ import ReactionSelectionNode
import EntityKeyboard
import LottieMetal
import TelegramAnimatedStickerNode
import AnimatedStickerNode
import ChatInputTextNode
import UndoUI
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
let sourceWindowFrame = fromView.convert(frame, to: nil)
@ -48,6 +51,8 @@ final class ChatSendMessageContextScreenComponent: Component {
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void
let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void
let reactionItems: [ReactionItem]?
let availableMessageEffects: AvailableMessageEffects?
let isPremium: Bool
init(
context: AccountContext,
@ -65,7 +70,9 @@ final class ChatSendMessageContextScreenComponent: Component {
completion: @escaping () -> Void,
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
reactionItems: [ReactionItem]?
reactionItems: [ReactionItem]?,
availableMessageEffects: AvailableMessageEffects?,
isPremium: Bool
) {
self.context = context
self.peerId = peerId
@ -83,6 +90,8 @@ final class ChatSendMessageContextScreenComponent: Component {
self.sendMessage = sendMessage
self.schedule = schedule
self.reactionItems = reactionItems
self.availableMessageEffects = availableMessageEffects
self.isPremium = isPremium
}
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
@ -115,7 +124,7 @@ final class ChatSendMessageContextScreenComponent: Component {
final class View: UIView {
private let backgroundView: BlurredBackgroundView
private var sendButton: HighlightTrackingButton?
private var sendButton: SendButton?
private var messageItemView: MessageItemView?
private var actionsStackNode: ContextControllerActionsStackNode?
private var reactionContextNode: ReactionContextNode?
@ -129,7 +138,10 @@ final class ChatSendMessageContextScreenComponent: Component {
private let messageEffectDisposable = MetaDisposable()
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
private var standaloneReactionAnimation: LottieMetalAnimatedStickerNode?
private var standaloneReactionAnimation: AnimatedStickerNode?
private var isLoadingEffectAnimation: Bool = false
private var loadEffectAnimationDisposable: Disposable?
private var presentationAnimationState: PresentationAnimationState = .initial
private var appliedAnimationState: PresentationAnimationState = .initial
@ -164,6 +176,7 @@ final class ChatSendMessageContextScreenComponent: Component {
deinit {
self.messageEffectDisposable.dispose()
self.loadEffectAnimationDisposable?.dispose()
}
@objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) {
@ -195,6 +208,20 @@ final class ChatSendMessageContextScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
}
private func requestUpdateOverlayWantsToBeBelowKeyboard(transition: ContainedViewLayoutTransition) {
guard let controller = self.environment?.controller() as? ChatSendMessageContextScreen else {
return
}
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
func wantsToBeBelowKeyboard() -> Bool {
if let reactionContextNode = self.reactionContextNode {
return reactionContextNode.wantsDisplayBelowKeyboard()
}
return false
}
func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
@ -254,23 +281,32 @@ final class ChatSendMessageContextScreenComponent: Component {
)
}
let sendButton: HighlightTrackingButton
let sendButton: SendButton
if let current = self.sendButton {
sendButton = current
} else {
sendButton = HighlightTrackingButton()
sendButton = SendButton()
sendButton.accessibilityLabel = environment.strings.MediaPicker_Send
sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside)
if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) {
/*if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) {
snapshotView.isUserInteractionEnabled = false
sendButton.addSubview(snapshotView)
}
}*/
self.sendButton = sendButton
self.addSubview(sendButton)
}
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
sendButton.update(
context: component.context,
presentationData: presentationData,
backgroundNode: component.wallpaperBackgroundNode,
isLoadingEffectAnimation: self.isLoadingEffectAnimation,
size: sourceSendButtonFrame.size,
transition: transition
)
let sendButtonScale: CGFloat
switch self.presentationAnimationState {
case .initial:
@ -279,55 +315,6 @@ final class ChatSendMessageContextScreenComponent: Component {
sendButtonScale = 1.0
}
let messageItemView: MessageItemView
if let current = self.messageItemView {
messageItemView = current
} else {
messageItemView = MessageItemView(frame: CGRect())
self.messageItemView = messageItemView
self.addSubview(messageItemView)
}
let textString: NSAttributedString
if let attributedText = component.textInputView.attributedText {
textString = attributedText
} else {
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
}
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
let explicitMessageBackgroundSize: CGSize?
switch self.presentationAnimationState {
case .initial:
explicitMessageBackgroundSize = sourceBackgroundSize
case .animatedOut:
if self.animateOutToEmpty {
explicitMessageBackgroundSize = nil
} else {
explicitMessageBackgroundSize = sourceBackgroundSize
}
case .animatedIn:
explicitMessageBackgroundSize = nil
}
let messageTextInsets = sourceMessageTextInsets
let messageItemSize = messageItemView.update(
context: component.context,
presentationData: presentationData,
backgroundNode: component.wallpaperBackgroundNode,
textString: textString,
textInsets: messageTextInsets,
explicitBackgroundSize: explicitMessageBackgroundSize,
maxTextWidth: localSourceTextInputViewFrame.width - 32.0,
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
transition: transition
)
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
let actionsStackNode: ContextControllerActionsStackNode
if let current = self.actionsStackNode {
actionsStackNode = current
@ -430,7 +417,73 @@ final class ChatSendMessageContextScreenComponent: Component {
presentation: .modal,
transition: transition.containedViewLayoutTransition
)
let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
let messageItemView: MessageItemView
if let current = self.messageItemView {
messageItemView = current
} else {
messageItemView = MessageItemView(frame: CGRect())
self.messageItemView = messageItemView
self.addSubview(messageItemView)
}
let textString: NSAttributedString
if let attributedText = component.textInputView.attributedText {
textString = attributedText
} else {
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
}
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
let explicitMessageBackgroundSize: CGSize?
switch self.presentationAnimationState {
case .initial:
explicitMessageBackgroundSize = sourceBackgroundSize
case .animatedOut:
if self.animateOutToEmpty {
explicitMessageBackgroundSize = nil
} else {
explicitMessageBackgroundSize = sourceBackgroundSize
}
case .animatedIn:
explicitMessageBackgroundSize = nil
}
let messageTextInsets = sourceMessageTextInsets
var maxTextHeight: CGFloat = availableSize.height - 8.0
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isExpanded {
maxTextHeight -= 300.0 + 8.0
} else {
maxTextHeight -= 60.0 + 14.0
}
}
maxTextHeight -= environment.statusBarHeight + 14.0
if environment.inputHeight != 0.0 {
maxTextHeight -= environment.inputHeight
} else {
maxTextHeight -= actionsStackSize.height
maxTextHeight -= environment.safeInsets.bottom
}
let messageItemSize = messageItemView.update(
context: component.context,
presentationData: presentationData,
backgroundNode: component.wallpaperBackgroundNode,
textString: textString,
sourceTextInputView: component.textInputView as? ChatInputTextView,
textInsets: messageTextInsets,
explicitBackgroundSize: explicitMessageBackgroundSize,
maxTextWidth: localSourceTextInputViewFrame.width,
maxTextHeight: maxTextHeight,
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
transition: transition
)
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
let reactionContextNode: ReactionContextNode
@ -442,7 +495,21 @@ final class ChatSendMessageContextScreenComponent: Component {
context: component.context,
animationCache: component.context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { item in
var icon: EmojiPagerContentComponent.Item.Icon = .none
if !component.isPremium, case let .custom(sourceEffectId) = item.reaction.rawValue, let availableMessageEffects = component.availableMessageEffects {
for messageEffect in availableMessageEffects.messageEffects {
if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId {
if messageEffect.isPremium {
icon = .locked
}
break
}
}
}
return ReactionContextItem.reaction(item: item, icon: icon)
},
selectedItems: Set(),
title: "Add an animated effect",
reactionsLocked: false,
@ -477,9 +544,7 @@ final class ChatSendMessageContextScreenComponent: Component {
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: Transition(transition))
}
self.requestUpdateOverlayWantsToBeBelowKeyboard(transition: transition)
}
)
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
@ -506,11 +571,8 @@ final class ChatSendMessageContextScreenComponent: Component {
return nil
}
self.messageEffectDisposable.set((combineLatest(
messageEffect,
ReactionContextNode.randomGenericReactionEffect(context: component.context)
)
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
self.messageEffectDisposable.set((messageEffect
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect in
guard let self, let component = self.component else {
return
}
@ -523,6 +585,8 @@ final class ChatSendMessageContextScreenComponent: Component {
if selectedMessageEffect.id == effectId {
self.selectedMessageEffect = nil
reactionContextNode.selectedItems = Set([])
self.loadEffectAnimationDisposable?.dispose()
self.isLoadingEffectAnimation = false
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
self.standaloneReactionAnimation = nil
@ -541,6 +605,8 @@ final class ChatSendMessageContextScreenComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
HapticFeedback().tap()
}
} else {
self.selectedMessageEffect = messageEffect
@ -548,10 +614,14 @@ final class ChatSendMessageContextScreenComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
HapticFeedback().tap()
}
guard let targetView = self.messageItemView?.effectIconView else {
return
self.loadEffectAnimationDisposable?.dispose()
self.isLoadingEffectAnimation = true
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
@ -561,56 +631,141 @@ final class ChatSendMessageContextScreenComponent: Component {
})
}
let _ = path
var customEffectResource: MediaResource?
var customEffectResource: (FileMediaReference, MediaResource)?
if let effectAnimation = messageEffect.effectAnimation {
customEffectResource = effectAnimation.resource
customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource)
} else {
let effectSticker = messageEffect.effectSticker
if let effectFile = effectSticker.videoThumbnails.first {
customEffectResource = effectFile.resource
customEffectResource = (FileMediaReference.standalone(media: effectSticker), effectFile.resource)
}
}
guard let customEffectResource else {
guard let (customEffectResourceFileReference, customEffectResource) = customEffectResource else {
return
}
let standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
standaloneReactionAnimation.isUserInteractionEnabled = false
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
self.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = effectFrame
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
self.addSubnode(standaloneReactionAnimation)
let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id)
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
guard let self else {
return
}
if let standaloneReactionAnimation {
standaloneReactionAnimation.removeFromSupernode()
if self.standaloneReactionAnimation === standaloneReactionAnimation {
self.standaloneReactionAnimation = nil
let context = component.context
var loadEffectAnimationSignal: Signal<Never, NoError>
loadEffectAnimationSignal = Signal { subscriber in
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start()
let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource)
|> filter { status in
if status == .Local {
return true
} else {
return false
}
}
|> take(1)).start(next: { _ in
subscriber.putCompletion()
})
return ActionDisposable {
fetchDisposable.dispose()
dataDisposabke.dispose()
}
}
standaloneReactionAnimation.visibility = true
#if DEBUG
loadEffectAnimationSignal = loadEffectAnimationSignal |> delay(1.0, queue: .mainQueue())
#endif
self.loadEffectAnimationDisposable = (loadEffectAnimationSignal
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let component = self.component else {
return
}
self.isLoadingEffectAnimation = false
guard let targetView = self.messageItemView?.effectIconView else {
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
return
}
let standaloneReactionAnimation: AnimatedStickerNode
#if targetEnvironment(simulator)
standaloneReactionAnimation = DirectAnimatedStickerNode()
#else
standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
#endif
standaloneReactionAnimation.isUserInteractionEnabled = false
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
self.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = effectFrame
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
self.addSubnode(standaloneReactionAnimation)
let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id)
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
guard let self else {
return
}
if let standaloneReactionAnimation {
standaloneReactionAnimation.removeFromSupernode()
if self.standaloneReactionAnimation === standaloneReactionAnimation {
self.standaloneReactionAnimation = nil
}
}
}
standaloneReactionAnimation.visibility = true
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
})
}))
}
reactionContextNode.premiumReactionsSelected = { [weak self] _ in
guard let self, let component = self.component else {
return
}
//TODO:localize
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
self.environment?.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .premiumPaywall(
title: nil,
text: "Subscribe to [TelegramPremium]() to add this animated effect.",
customUndoText: nil,
timeout: nil,
linkAction: nil
),
elevatedLayout: false,
action: { [weak self] action in
guard let self, let component = self.component else {
return false
}
if case .info = action {
self.window?.endEditing(true)
//TODO:localize
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .animatedEmoji, forceDark: false, dismissed: nil)
let _ = premiumController
//parentNavigationController.pushViewController(premiumController)
}
return false
}
), in: .current)
}
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = false
reactionContextNode.forceDark = false
reactionContextNode.isMessageEffects = true
self.reactionContextNode = reactionContextNode
self.addSubview(reactionContextNode.view)
}
}
let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX, y: sourceSendButtonFrame.minY), size: sourceSendButtonFrame.size)
var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 8.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize)
var readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
@ -622,6 +777,13 @@ final class ChatSendMessageContextScreenComponent: Component {
readySendButtonFrame.origin.y -= bottomOverflow
}
let inputCoverOverflow = readyMessageItemFrame.maxY + 7.0 - (availableSize.height - environment.inputHeight)
if inputCoverOverflow > 0.0 {
readyMessageItemFrame.origin.y -= inputCoverOverflow
readyActionsStackFrame.origin.y -= inputCoverOverflow
readySendButtonFrame.origin.y -= inputCoverOverflow
}
let messageItemFrame: CGRect
let actionsStackFrame: CGRect
let sendButtonFrame: CGRect
@ -697,6 +859,7 @@ final class ChatSendMessageContextScreenComponent: Component {
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
transition.setScale(view: sendButton, scale: sendButtonScale)
sendButton.updateGlobalRect(rect: sendButtonFrame, within: availableSize, transition: transition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
@ -769,6 +932,14 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
override public var overlayWantsToBeBelowKeyboard: Bool {
if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View {
return componentView.wantsToBeBelowKeyboard()
} else {
return false
}
}
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
@ -786,7 +957,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
completion: @escaping () -> Void,
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
reactionItems: [ReactionItem]?
reactionItems: [ReactionItem]?,
availableMessageEffects: AvailableMessageEffects?,
isPremium: Bool
) {
self.context = context
@ -808,7 +981,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
completion: completion,
sendMessage: sendMessage,
schedule: schedule,
reactionItems: reactionItems
reactionItems: reactionItems,
availableMessageEffects: availableMessageEffects,
isPremium: isPremium
),
navigationBarAppearance: .none,
statusBarStyle: .none,

View File

@ -17,6 +17,7 @@ import WallpaperBackgroundNode
import MultilineTextWithEntitiesComponent
import ReactionButtonListComponent
import MultilineTextComponent
import ChatInputTextNode
private final class EffectIcon: Component {
enum Content: Equatable {
@ -135,7 +136,8 @@ final class MessageItemView: UIView {
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
private let backgroundNode: ChatMessageBackground
private let text = ComponentView<Empty>()
private let textClippingContainer: UIView
private var textNode: ChatInputTextNode?
private var effectIcon: ComponentView<Empty>?
var effectIconView: UIView? {
@ -150,10 +152,15 @@ final class MessageItemView: UIView {
self.backgroundNode = ChatMessageBackground()
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
self.textClippingContainer = UIView()
self.textClippingContainer.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.backgroundWallpaperNode.view)
self.addSubview(self.backgroundNode.view)
self.addSubview(self.textClippingContainer)
}
required init(coder: NSCoder) {
@ -165,9 +172,11 @@ final class MessageItemView: UIView {
presentationData: PresentationData,
backgroundNode: WallpaperBackgroundNode?,
textString: NSAttributedString,
sourceTextInputView: ChatInputTextView?,
textInsets: UIEdgeInsets,
explicitBackgroundSize: CGSize?,
maxTextWidth: CGFloat,
maxTextHeight: CGFloat,
effect: AvailableMessageEffects.MessageEffect?,
transition: Transition
) -> CGSize {
@ -201,33 +210,84 @@ final class MessageItemView: UIView {
if let effectIconSize {
textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height))
}
let _ = textCutout
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
placeholderColor: presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper,
text: .plain(textString),
maximumNumberOfLines: 0,
lineSpacing: 0.0,
cutout: textCutout,
insets: UIEdgeInsets()
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 20000.0)
let textNode: ChatInputTextNode
if let current = self.textNode {
textNode = current
} else {
textNode = ChatInputTextNode(disableTiling: true)
textNode.textView.isScrollEnabled = false
textNode.isUserInteractionEnabled = false
self.textNode = textNode
self.textClippingContainer.addSubview(textNode.view)
if let sourceTextInputView {
textNode.textView.defaultTextContainerInset = sourceTextInputView.defaultTextContainerInset
}
let messageAttributedText = NSMutableAttributedString(attributedString: textString)
messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length))
textNode.attributedText = messageAttributedText
}
let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor
let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle
if let sourceTextInputView, let textTheme = sourceTextInputView.theme {
switch textTheme.quote.lineStyle {
case .solid:
mappedLineStyle = .solid(color: mainColor)
case .doubleDashed:
mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear)
case .tripleDashed:
mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear)
}
} else {
mappedLineStyle = .solid(color: mainColor)
}
textNode.textView.theme = ChatInputTextView.Theme(
quote: ChatInputTextView.Theme.Quote(
background: mainColor.withMultipliedAlpha(0.1),
foreground: mainColor,
lineStyle: mappedLineStyle,
codeBackground: mainColor.withMultipliedAlpha(0.1),
codeForeground: mainColor
)
)
let size = CGSize(width: textSize.width + textInsets.left + textInsets.right, height: textSize.height + textInsets.top + textInsets.bottom)
let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0)
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
textView.frame = textFrame
var currentRightInset: CGFloat = 0.0
if let sourceTextInputView {
currentRightInset = sourceTextInputView.currentRightInset
}
let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset)
textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight))
let textBoundingRect = textNode.textView.currentTextBoundingRect().integral
let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral
let textWidth = textBoundingRect.width
let textSize = CGSize(width: textWidth, height: textHeight)
var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom)
let effectInset: CGFloat = 12.0
if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset {
if lastLineBoundingRect != textBoundingRect {
positionedTextSize.height += 11.0
} else {
positionedTextSize.width += effectInset
}
}
let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom)
positionedTextSize.height = min(positionedTextSize.height, maxTextHeight)
let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom)
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize)
let chatTheme: ChatPresentationThemeData
if let current = self.chatTheme, current.theme === presentationData.theme {
@ -260,6 +320,21 @@ final class MessageItemView: UIView {
let previousSize = self.currentSize
self.currentSize = backgroundSize
let textClippingContainerFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: backgroundSize.width - 1.0 - 7.0, height: backgroundSize.height - 1.0 - 1.0))
var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size)
if explicitBackgroundSize != nil, let sourceTextInputView {
textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y
} else {
textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0
textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y)
}
transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center)
transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds)
textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))
if let effectIcon = self.effectIcon, let effectIconSize {
if let effectIconView = effectIcon.view {
var animateIn = false

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

View File

@ -22,4 +22,10 @@ public class PortalView {
portalSuperlayer.insertSublayer(self.view.layer, at: UInt32(index))
}
}
public func reloadPortal() {
if let sourceView = self.sourceView as? PortalSourceView {
self.reloadPortal(sourceView: sourceView)
}
}
}

View File

@ -133,7 +133,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView {
context: self.context,
animationCache: self.context.animationCache,
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: Set(),
title: nil,
reactionsLocked: false,

View File

@ -41,6 +41,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
private let backgroundView: BlurredBackgroundView
private(set) var vibrancyEffectView: UIVisualEffectView?
let vibrantExpandedContentContainer: UIView
private let maskLayer: SimpleLayer
private let backgroundClippingLayer: SimpleLayer
@ -84,6 +85,8 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
self.smallCircleLayer.cornerCurve = .circular
}
self.vibrantExpandedContentContainer = UIView()
super.init()
self.layer.addSublayer(self.backgroundShadowLayer)
@ -146,6 +149,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView
vibrancyEffectView.contentView.addSubview(self.vibrantExpandedContentContainer)
self.backgroundView.addSubview(vibrancyEffectView)
}
}

View File

@ -82,9 +82,9 @@ public enum ReactionContextItem: Equatable {
} else {
return false
}
case let .reaction(lhsReaction):
if case let .reaction(rhsReaction) = rhs {
return lhsReaction.reaction == rhsReaction.reaction
case let .reaction(lhsReaction, lhsIcon):
if case let .reaction(rhsReaction, rhsIcon) = rhs {
return lhsReaction.reaction == rhsReaction.reaction && lhsIcon == rhsIcon
} else {
return false
}
@ -98,11 +98,11 @@ public enum ReactionContextItem: Equatable {
}
case staticEmoji(String)
case reaction(ReactionItem)
case reaction(item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon)
case premium
public var reaction: ReactionItem.Reaction? {
if case let .reaction(item) = self {
if case let .reaction(item, _) = self {
return item.reaction
} else {
return nil
@ -386,6 +386,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
public var forceDark: Bool = false
public var hideBackground: Bool = false
public var isMessageEffects: Bool = false
private var didAnimateIn: Bool = false
public private(set) var isAnimatingOut: Bool = false
public private(set) var isAnimatingOutToReaction: Bool = false
@ -829,6 +831,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
public func wantsDisplayBelowKeyboard() -> Bool {
if let emojiView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
return emojiView.wantsDisplayBelowKeyboard()
} else if let stickersView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("stickers"))) as? EmojiPagerContentComponent.View {
return stickersView.wantsDisplayBelowKeyboard()
} else {
return false
}
@ -1047,8 +1051,16 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
itemTransition = .immediate
switch self.items[i] {
case let .reaction(item):
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: self.reactionsLocked)
case let .reaction(item, icon):
var isLocked = self.reactionsLocked
switch icon {
case .locked:
isLocked = true
default:
break
}
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, icon: icon, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: isLocked)
maskNode = nil
case let .staticEmoji(emoji):
itemNode = EmojiItemNode(theme: self.presentationData.theme, emoji: emoji)
@ -1498,6 +1510,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
animationOffsetY += 54.0
} else if self.alwaysAllowPremiumReactions {
animationOffsetY += 4.0
} else if self.isMessageEffects {
animationOffsetY += 54.0
transition.animatePositionAdditive(layer: self.backgroundNode.vibrantExpandedContentContainer.layer, offset: CGPoint(x: 0.0, y: -animationOffsetY + floorToScreenPixels(self.animateFromExtensionDistance / 2.0)))
} else {
animationOffsetY += 46.0 + 54.0 - 4.0
}
@ -1814,115 +1829,147 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
}
|> distinctUntilChanged
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then(
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
($0, true)
}
)
let resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
if isEmojiOnly {
var items: [EmojiPagerContentComponent.Item] = []
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
for emojiString in list {
if allEmoticons[emojiString] != nil {
let item = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(emojiString),
itemFile: nil,
subgroupId: nil,
icon: .none,
tintMode: .none
)
items.append(item)
}
let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError>
if self.isMessageEffects {
resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
))
return .single(resultGroups)
} else {
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
context.engine.stickers.availableReactions() |> take(1),
hasPremium |> take(1),
remotePacksSignal
)
|> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = []
return context.availableMessageEffects
|> take(1)
|> mapToSignal { availableMessageEffects -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
guard let availableMessageEffects else {
return .single([])
}
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
var filteredEffects: [AvailableMessageEffects.MessageEffect] = []
for messageEffect in availableMessageEffects.messageEffects {
if allEmoticons[messageEffect.emoticon] != nil {
filteredEffects.append(messageEffect)
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if !item.file.isPremiumEmoji || hasPremium {
if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
default:
break
}
var reactionEffects: [AvailableMessageEffects.MessageEffect] = []
var stickerEffects: [AvailableMessageEffects.MessageEffect] = []
for messageEffect in filteredEffects {
if messageEffect.effectAnimation != nil {
reactionEffects.append(messageEffect)
} else {
stickerEffects.append(messageEffect)
}
}
var items: [EmojiPagerContentComponent.Item] = []
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String?
var subtitle: String?
var actionButtonTitle: String?
var isPremiumLocked: Bool
var isFeatured: Bool
var displayPremiumBadges: Bool
var hasEdit: Bool
var headerItem: EntityKeyboardAnimationData?
var items: [EmojiPagerContentComponent.Item]
}
var existingIds = Set<MediaId>()
for item in result {
if let itemFile = item.1 {
if existingIds.contains(itemFile.fileId) {
continue
var resultGroups: [ItemGroup] = []
var resultGroupIndexById: [AnyHashable: Int] = [:]
for i in 0 ..< 2 {
let groupId = i == 0 ? "reactions" : "stickers"
for item in i == 0 ? reactionEffects : stickerEffects {
let itemFile: TelegramMediaFile = item.effectSticker
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if itemFile.isCustomTemplateEmoji {
tintMode = .primary
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
itemFile: itemFile,
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
tintMode: tintMode
)
items.append(item)
if let groupIndex = resultGroupIndexById[groupId] {
resultGroups[groupIndex].items.append(resultItem)
} else {
resultGroupIndexById[groupId] = resultGroups.count
//TODO:localize
resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem]))
}
}
}
let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
let hasClear = false
let isEmbedded = false
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
badge: nil,
actionButtonTitle: group.actionButtonTitle,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: isEmbedded,
hasClear: hasClear,
hasEdit: group.hasEdit,
collapsedLineCount: nil,
displayPremiumBadges: group.displayPremiumBadges,
headerItem: group.headerItem,
fillWithLoadingPlaceholders: false,
items: group.items
)
}
return .single(allItemGroups)
}
}
} else {
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then(
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
($0, true)
}
)
resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
if isEmojiOnly {
var items: [EmojiPagerContentComponent.Item] = []
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
for emojiString in list {
if allEmoticons[emojiString] != nil {
let item = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(emojiString),
itemFile: nil,
subgroupId: nil,
icon: .none,
tintMode: .none
)
items.append(item)
}
}
}
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
@ -1942,59 +1989,138 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
fillWithLoadingPlaceholders: false,
items: items
))
for (collectionId, info, _, _) in foundPacks.sets.infos {
if let info = info as? StickerPackCollectionInfo {
var topItems: [StickerPackItem] = []
for e in foundPacks.sets.entries {
if let item = e.item as? StickerPackItem {
if e.index.collectionId == collectionId {
topItems.append(item)
return .single(resultGroups)
} else {
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
context.engine.stickers.availableReactions() |> take(1),
hasPremium |> take(1),
remotePacksSignal
)
|> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = []
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if !item.file.isPremiumEmoji || hasPremium {
if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
default:
break
}
}
var groupItems: [EmojiPagerContentComponent.Item] = []
for item in topItems {
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if item.file.isCustomTemplateEmoji {
tintMode = .primary
}
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in result {
if let itemFile = item.1 {
if existingIds.contains(itemFile.fileId) {
continue
}
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: tintMode
tintMode: animationData.isTemplate ? .primary : .none
)
groupItems.append(resultItem)
items.append(item)
}
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: AnyHashable(info.id),
groupId: AnyHashable(info.id),
title: info.title,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: 3,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: groupItems
))
}
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
))
for (collectionId, info, _, _) in foundPacks.sets.infos {
if let info = info as? StickerPackCollectionInfo {
var topItems: [StickerPackItem] = []
for e in foundPacks.sets.entries {
if let item = e.item as? StickerPackItem {
if e.index.collectionId == collectionId {
topItems.append(item)
}
}
}
var groupItems: [EmojiPagerContentComponent.Item] = []
for item in topItems {
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if item.file.isCustomTemplateEmoji {
tintMode = .primary
}
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
tintMode: tintMode
)
groupItems.append(resultItem)
}
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: AnyHashable(info.id),
groupId: AnyHashable(info.id),
title: info.title,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: 3,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: groupItems
))
}
}
return resultGroups
}
return resultGroups
}
}
}
@ -2013,45 +2139,156 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
}))
}
case let .category(value):
let resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
let context = self.context
let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError>
if self.isMessageEffects {
let keywords: Signal<[String], NoError> = .single(value.identifiers)
resultSignal = keywords
|> mapToSignal { keywords -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
allEmoticons[keyword] = keyword
}
return context.availableMessageEffects
|> take(1)
|> mapToSignal { availableMessageEffects -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
guard let availableMessageEffects else {
return .single(([], true))
}
var filteredEffects: [AvailableMessageEffects.MessageEffect] = []
for messageEffect in availableMessageEffects.messageEffects {
if allEmoticons[messageEffect.emoticon] != nil {
filteredEffects.append(messageEffect)
}
}
var reactionEffects: [AvailableMessageEffects.MessageEffect] = []
var stickerEffects: [AvailableMessageEffects.MessageEffect] = []
for messageEffect in filteredEffects {
if messageEffect.effectAnimation != nil {
reactionEffects.append(messageEffect)
} else {
stickerEffects.append(messageEffect)
}
}
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String?
var subtitle: String?
var actionButtonTitle: String?
var isPremiumLocked: Bool
var isFeatured: Bool
var displayPremiumBadges: Bool
var hasEdit: Bool
var headerItem: EntityKeyboardAnimationData?
var items: [EmojiPagerContentComponent.Item]
}
var resultGroups: [ItemGroup] = []
var resultGroupIndexById: [AnyHashable: Int] = [:]
for i in 0 ..< 2 {
let groupId = i == 0 ? "reactions" : "stickers"
for item in i == 0 ? reactionEffects : stickerEffects {
let itemFile: TelegramMediaFile = item.effectSticker
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if itemFile.isCustomTemplateEmoji {
tintMode = .primary
}
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile,
subgroupId: nil,
icon: .none,
tintMode: tintMode
)
if let groupIndex = resultGroupIndexById[groupId] {
resultGroups[groupIndex].items.append(resultItem)
} else {
resultGroupIndexById[groupId] = resultGroups.count
//TODO:localize
resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem]))
}
}
}
let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
let hasClear = false
let isEmbedded = false
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
badge: nil,
actionButtonTitle: group.actionButtonTitle,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: isEmbedded,
hasClear: hasClear,
hasEdit: group.hasEdit,
collapsedLineCount: nil,
displayPremiumBadges: group.displayPremiumBadges,
headerItem: group.headerItem,
fillWithLoadingPlaceholders: false,
items: group.items
)
}
return .single((allItemGroups, true))
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], isFinalResult))
} else {
resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], isFinalResult))
}
}
var version = 0
@ -2101,7 +2338,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
peekBehavior: nil,
customLayout: emojiContentLayout,
externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground(
effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView
effectContainerView: self.backgroundNode.vibrantExpandedContentContainer
),
externalExpansionView: self.view,
customContentView: nil,
@ -2310,7 +2547,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
}
if let customReactionSource = self.customReactionSource {
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, icon: .none, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
if let contents = customReactionSource.layer.contents {
itemNode.setCustomContents(contents: contents)
}
@ -2737,11 +2974,13 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring))
} else if let reaction = self.reaction(at: point) {
switch reaction {
case let .reaction(reactionItem):
case let .reaction(reactionItem, icon):
if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable {
self.premiumReactionsSelected?(reactionItem.stillAnimation)
} else if self.reactionsLocked {
self.premiumReactionsSelected?(reactionItem.stillAnimation)
} else if case .locked = icon {
self.premiumReactionsSelected?(reactionItem.stillAnimation)
} else {
self.reactionSelected?(reactionItem.updateMessageReaction, false)
}
@ -2915,7 +3154,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
if !itemNode.isAnimationLoaded {
return nil
}
return .reaction(itemNode.item)
return .reaction(item: itemNode.item, icon: itemNode.icon)
} else if let itemNode = itemNode as? EmojiItemNode {
return .staticEmoji(itemNode.emoji)
} else if let _ = itemNode as? PremiumReactionsNode {
@ -2999,7 +3238,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
itemNode = currentItemNode
} else {
let animationRenderer = MultiAnimationRendererImpl()
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
}
self.itemNode = itemNode
} else {

View File

@ -14,6 +14,7 @@ import AnimationCache
import MultiAnimationRenderer
import ShimmerEffect
import GenerateStickerPlaceholderImage
import EntityKeyboard
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
@ -52,13 +53,14 @@ protocol ReactionItemNode: ASDisplayNode {
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition)
}
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 12.0, color: .white)!.withRenderingMode(.alwaysTemplate)
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let context: AccountContext
let theme: PresentationTheme
let item: ReactionItem
let icon: EmojiPagerContentComponent.Item.Icon
private let loopIdle: Bool
private let isLocked: Bool
private let hasAppearAnimation: Bool
@ -102,10 +104,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
return self.staticAnimationNode.currentFrameImage != nil
}
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
self.context = context
self.theme = theme
self.item = item
self.icon = icon
self.loopIdle = loopIdle
self.isLocked = isLocked
self.hasAppearAnimation = hasAppearAnimation
@ -475,11 +478,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
}
if let lockBackgroundView = self.lockBackgroundView, let lockIconView = self.lockIconView, let iconImage = lockIconView.image {
let lockSize: CGFloat = 12.0
let lockSize: CGFloat = 16.0
let iconBackgroundFrame = CGRect(origin: CGPoint(x: animationFrame.maxX - lockSize, y: animationFrame.maxY - lockSize), size: CGSize(width: lockSize, height: lockSize))
transition.updateFrame(view: lockBackgroundView, frame: iconBackgroundFrame)
let iconFactor: CGFloat = 0.7
let iconFactor: CGFloat = 1.0
let iconImageSize = CGSize(width: floor(iconImage.size.width * iconFactor), height: floor(iconImage.size.height * iconFactor))
transition.updateFrame(view: lockIconView, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floorToScreenPixels((iconBackgroundFrame.width - iconImageSize.width) * 0.5), y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - iconImageSize.height) * 0.5)), size: iconImageSize))

View File

@ -156,7 +156,7 @@ private extension AvailableMessageEffects.MessageEffect {
return nil
}
let isPremium = (flags & (1 << 3)) != 0
let isPremium = (flags & (1 << 2)) != 0
self.init(
id: id,
isPremium: isPremium,
@ -239,7 +239,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
break
}
var signals: [Signal<Never, NoError>] = []
/*var signals: [Signal<Never, NoError>] = []
if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) {
var resources: [MediaResource] = []
@ -271,7 +271,9 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ
}
return combineLatest(signals)
|> ignoreValues
|> ignoreValues*/
return .complete()
}
|> switchToLatest
})

View File

@ -312,6 +312,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
}
}
public var currentRightInset: CGFloat {
return self.customTextContainer.rightInset
}
private var didInitializePrimaryInputLanguage: Bool = false
public var initialPrimaryLanguage: String?
@ -656,6 +660,45 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
self.isUpdatingLayout = false
}
public func currentTextBoundingRect() -> CGRect {
let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil)
var boundingRect = CGRect()
var startIndex = glyphRange.lowerBound
while startIndex < glyphRange.upperBound {
var effectiveRange = NSRange(location: NSNotFound, length: 0)
let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange)
if boundingRect.isEmpty {
boundingRect = rect
} else {
boundingRect = boundingRect.union(rect)
}
if effectiveRange.location != NSNotFound {
startIndex = max(startIndex + 1, effectiveRange.upperBound)
} else {
break
}
}
return boundingRect
}
public func lastLineBoundingRect() -> CGRect {
let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil)
var boundingRect = CGRect()
var startIndex = glyphRange.lowerBound
while startIndex < glyphRange.upperBound {
var effectiveRange = NSRange(location: NSNotFound, length: 0)
let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange)
boundingRect = rect
if effectiveRange.location != NSNotFound {
startIndex = max(startIndex + 1, effectiveRange.upperBound)
} else {
break
}
}
return boundingRect
}
public func updateTextElements() {
var blockQuoteIndex = 0
var validBlockQuotes: [Int] = []

View File

@ -633,6 +633,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
self.visibilityStatus = self.visibility != .none
self.updateVisibility()
}
}
}
@ -648,8 +650,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
containerSize: credibilityIconView.bounds.size
)
}
self.updateVisibility()
}
}
}
@ -5840,7 +5840,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
do {
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
let additionalAnimationNode = LottieMetalAnimatedStickerNode()
let additionalAnimationNode: AnimatedStickerNode
#if targetEnvironment(simulator)
additionalAnimationNode = DirectAnimatedStickerNode()
#else
additionalAnimationNode = LottieMetalAnimatedStickerNode()
#endif
additionalAnimationNode.updateLayout(size: animationSize)
additionalAnimationNode.setup(source: source, width: Int(animationSize.width), height: Int(animationSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
var animationFrame: CGRect
@ -5925,32 +5930,46 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations
var isPlaying = true
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if self.forceStopAnimations {
isPlaying = false
}
if !isPlaying {
self.removeAdditionalAnimations()
}
var alreadySeen = true
if item.message.flags.contains(.Incoming) {
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
if unreadRange.contains(item.message.id.id) {
if isPlaying {
var alreadySeen = true
if item.message.flags.contains(.Incoming) {
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
if unreadRange.contains(item.message.id.id) {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
}
}
}
} else {
if self.didChangeFromPendingToSent {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
}
}
}
} else {
if self.didChangeFromPendingToSent {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = false
}
}
}
if !alreadySeen {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
self.playMessageEffect(force: false)
if !alreadySeen {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
self.playMessageEffect(force: false)
}
}
}
}

View File

@ -257,7 +257,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
context: context,
animationCache: context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: actions.editTags,
title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag,
reactionsLocked: false,

View File

@ -31,7 +31,7 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio
context: context,
animationCache: context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: Set(),
title: isSingleMessage ? presentationData.strings.Chat_ForwardToSavedMessageTagSelectionTitle : presentationData.strings.Chat_ForwardToSavedMessagesTagSelectionTitle,
reactionsLocked: false,

View File

@ -47,6 +47,7 @@ swift_library(
"//submodules/rlottie:RLottieBinding",
"//submodules/lottie-ios:Lottie",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/TelegramUIPreferences",
],
visibility = [
"//visibility:public",

View File

@ -602,14 +602,23 @@ private class PassthroughShapeLayer: CAShapeLayer {
}
}
private let itemBadgeTextFont: UIFont = {
return Font.regular(10.0)
}()
private final class PremiumBadgeView: UIView {
private let context: AccountContext
private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
let contentLayer: SimpleLayer
private let overlayColorLayer: SimpleLayer
private let iconLayer: SimpleLayer
private var customFileLayer: InlineFileIconLayer?
init() {
init(context: AccountContext) {
self.context = context
self.contentLayer = SimpleLayer()
self.contentLayer.contentsGravity = .resize
self.contentLayer.masksToBounds = true
@ -641,6 +650,47 @@ private final class PremiumBadgeView: UIView {
self.iconLayer.contents = featuredBadgeIcon?.cgImage
case .locked:
self.iconLayer.contents = lockedBadgeIcon?.cgImage
case let .text(text):
let string = NSAttributedString(string: text, font: itemBadgeTextFont)
let size = CGSize(width: 12.0, height: 12.0)
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5)))
UIGraphicsPopContext()
})
self.iconLayer.contents = image?.cgImage
case .customFile:
self.iconLayer.contents = nil
}
if case let .customFile(customFile) = badge {
let customFileLayer: InlineFileIconLayer
if let current = self.customFileLayer {
customFileLayer = current
} else {
customFileLayer = InlineFileIconLayer(
context: self.context,
userLocation: .other,
attemptSynchronousLoad: false,
file: customFile,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
unique: false,
placeholderColor: .clear,
pointSize: CGSize(width: 18.0, height: 18.0),
dynamicColor: nil
)
self.customFileLayer = customFileLayer
self.layer.addSublayer(customFileLayer)
}
let _ = customFileLayer
} else {
if let customFileLayer = self.customFileLayer {
self.customFileLayer = nil
customFileLayer.removeFromSuperlayer()
}
}
}
@ -652,6 +702,17 @@ private final class PremiumBadgeView: UIView {
iconInset = 0.0
case .locked:
iconInset = 0.0
case .text, .customFile:
iconInset = 0.0
}
switch badge {
case .text, .customFile:
self.contentLayer.isHidden = true
self.overlayColorLayer.isHidden = true
default:
self.contentLayer.isHidden = false
self.overlayColorLayer.isHidden = false
}
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
@ -663,6 +724,11 @@ private final class PremiumBadgeView: UIView {
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
if let customFileLayer = self.customFileLayer {
let iconSize = CGSize(width: 18.0, height: 18.0)
transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize))
}
}
}
@ -2608,6 +2674,8 @@ public final class EmojiPagerContentComponent: Component {
case none
case locked
case premium
case text(String)
case customFile(TelegramMediaFile)
}
public enum TintMode: Equatable {
@ -3448,13 +3516,16 @@ public final class EmojiPagerContentComponent: Component {
}
}
enum Badge {
enum Badge: Equatable {
case premium
case locked
case featured
case text(String)
case customFile(TelegramMediaFile)
}
public let item: Item
private let context: AccountContext
private var content: ItemContent
private var theme: PresentationTheme?
@ -3566,6 +3637,7 @@ public final class EmojiPagerContentComponent: Component {
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
) {
self.item = item
self.context = context
self.content = content
self.placeholderColor = placeholderColor
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
@ -3717,6 +3789,7 @@ public final class EmojiPagerContentComponent: Component {
preconditionFailure()
}
self.context = layer.context
self.item = layer.item
self.content = layer.content
@ -3837,7 +3910,7 @@ public final class EmojiPagerContentComponent: Component {
premiumBadgeView = current
} else {
badgeTransition = .immediate
premiumBadgeView = PremiumBadgeView()
premiumBadgeView = PremiumBadgeView(context: self.context)
self.premiumBadgeView = premiumBadgeView
self.addSublayer(premiumBadgeView.layer)
}
@ -6202,6 +6275,10 @@ public final class EmojiPagerContentComponent: Component {
badge = .locked
case .premium:
badge = .premium
case let .text(value):
badge = .text(value)
case let .customFile(customFile):
badge = .customFile(customFile)
}
}

View File

@ -2176,7 +2176,7 @@ public extension EmojiPagerContentComponent {
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let searchCategories: Signal<EmojiSearchCategories?, NoError> = .single(nil)
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
return combineLatest(
hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false),
@ -2225,13 +2225,30 @@ public extension EmojiPagerContentComponent {
tintMode = .primary
}
let icon: EmojiPagerContentComponent.Item.Icon
if i == 0 {
if !hasPremium && item.isPremium {
icon = .locked
} else {
icon = .none
}
} else {
if !hasPremium && item.isPremium {
icon = .locked
} else if let staticIcon = item.staticIcon {
icon = .customFile(staticIcon)
} else {
icon = .text(item.emoticon)
}
}
let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile,
subgroupId: nil,
icon: .none,
icon: icon,
tintMode: tintMode
)

View File

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

View File

@ -15,7 +15,6 @@ objc_library(
copts = [
"-Werror",
"-I{}/Sources".format(package_name()),
"-O2",
],
hdrs = glob([
"PublicHeaders/**/*.h",
@ -32,3 +31,18 @@ objc_library(
"//visibility:public",
],
)
cc_library(
name = "LottieCppBinding",
srcs = [],
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
copts = [],
visibility = ["//visibility:public"],
linkstatic = 1,
tags = ["swift_module=LottieCppBinding"],
)

View File

@ -21,6 +21,30 @@ class RenderTreeNode;
extern "C" {
#endif
typedef struct {
CGRect bounds;
CGPoint position;
CATransform3D transform;
double opacity;
bool masksToBounds;
bool isHidden;
} LottieRenderNodeLayerData;
typedef struct {
int64_t internalId;
bool isValid;
LottieRenderNodeLayerData layer;
CGRect globalRect;
CGRect localRect;
CATransform3D globalTransform;
bool drawsContent;
bool hasSimpleContents;
int drawContentDescendants;
bool isInvertedMatte;
int64_t maskId;
int subnodeCount;
} LottieRenderNodeProxy;
@interface LottieAnimationContainer : NSObject
@property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation;
@ -34,6 +58,10 @@ extern "C" {
- (std::shared_ptr<lottie::RenderTreeNode>)internalGetRootRenderTreeNode;
#endif
- (int64_t)getRootRenderNodeProxy;
- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct));
- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct));
@end
#ifdef __cplusplus

View File

@ -10,19 +10,10 @@ void batchInterpolate(std::vector<PathElement> const &from, std::vector<PathElem
elementCount = (int)to.size();
}
if (sizeof(PathElement) == 8 * 2 * 3) {
resultPath.setElementCount(elementCount);
vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3);
} else {
for (int i = 0; i < elementCount; i++) {
const auto &fromVertex = from[i].vertex;
const auto &toVertex = to[i].vertex;
auto vertex = ValueInterpolator<CurveVertex>::interpolate(fromVertex, toVertex, amount);
resultPath.updateVertex(vertex, i, false);
}
}
static_assert(sizeof(PathElement) == 8 * 2 * 3);
resultPath.setElementCount(elementCount);
vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3);
}
}

View File

@ -178,15 +178,6 @@ public:
}
} else {
batchInterpolate(value.elements(), to.elements(), resultPath, amount);
/*for (int i = 0; i < elementCount; i++) {
const auto &fromVertex = value.elements()[i].vertex;
const auto &toVertex = to.elements()[i].vertex;
auto vertex = ValueInterpolator<CurveVertex>::interpolate(fromVertex, toVertex, amount);
resultPath.updateVertex(vertex, i, false);
}*/
}
}
};

View File

@ -4,6 +4,7 @@
#include "LottieAnimationInternal.h"
#include "RenderNode.hpp"
#include "LottieRenderTreeInternal.h"
#include <LottieCpp/VectorsCocoa.h>
namespace lottie {
@ -367,6 +368,48 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
return renderNode;
}
- (int64_t)getRootRenderNodeProxy {
std::shared_ptr<lottie::RenderTreeNode> renderNode = [self internalGetRootRenderTreeNode];
return (int64_t)renderNode.get();
}
- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)) {
lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId;
LottieRenderNodeProxy result;
result.internalId = nodeId;
result.isValid = node->renderData.isValid;
result.layer.bounds = CGRectMake(node->renderData.layer._bounds.x, node->renderData.layer._bounds.y, node->renderData.layer._bounds.width, node->renderData.layer._bounds.height);
result.layer.position = CGPointMake(node->renderData.layer._position.x, node->renderData.layer._position.y);
result.layer.transform = lottie::nativeTransform(node->renderData.layer._transform);
result.layer.opacity = node->renderData.layer._opacity;
result.layer.masksToBounds = node->renderData.layer._masksToBounds;
result.layer.isHidden = node->renderData.layer._isHidden;
result.globalRect = CGRectMake(node->renderData.globalRect.x, node->renderData.globalRect.y, node->renderData.globalRect.width, node->renderData.globalRect.height);
result.localRect = CGRectMake(node->renderData.localRect.x, node->renderData.localRect.y, node->renderData.localRect.width, node->renderData.localRect.height);
result.globalTransform = lottie::nativeTransform(node->renderData.globalTransform);
result.drawsContent = node->renderData.drawsContent;
result.hasSimpleContents = node->renderData.drawContentDescendants <= 1;
result.drawContentDescendants = node->renderData.drawContentDescendants;
result.isInvertedMatte = node->renderData.isInvertedMatte;
if (node->mask()) {
result.maskId = (int64_t)node->mask().get();
} else {
result.maskId = 0;
}
result.subnodeCount = (int)node->subnodes().size();
return result;
}
- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)) {
lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId;
return [self getRenderNodeProxyById:(int64_t)node->subnodes()[index].get()];
}
@end
@implementation LottieAnimationContainer (Internal)

View File

@ -52,6 +52,59 @@ private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount:
return device.makeTexture(descriptor: textureDescriptor)!
}
public func cacheLottieMetalAnimation(path: String) -> Data? {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
if let lottieAnimation = LottieAnimation(data: decompressedData) {
let animationContainer = LottieAnimationContainer(animation: lottieAnimation)
let startTime = CFAbsoluteTimeGetCurrent()
let buffer = WriteBuffer()
var frameMapping = SerializedLottieMetalFrameMapping()
frameMapping.size = animationContainer.animation.size
frameMapping.frameCount = animationContainer.animation.frameCount
frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond
for i in 0 ..< frameMapping.frameCount {
frameMapping.frameRanges[i] = 0 ..< 1
}
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
for i in 0 ..< animationContainer.animation.frameCount {
animationContainer.update(i)
let frameRangeStart = buffer.length
if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) {
serializeNode(buffer: buffer, node: node)
let frameRangeEnd = buffer.length
frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd
}
}
let previousLength = buffer.length
buffer.length = 0
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
buffer.length = previousLength
buffer.trim()
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
let zippedData = TGGZipData(buffer.data, 1.0)
print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
return zippedData
}
}
return nil
}
public func parseCachedLottieMetalAnimation(data: Data) -> LottieContentLayer.Content? {
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
let serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData)
return .serialized(frameMapping: serializedFrames.0, data: serializedFrames.1)
}
return nil
}
private final class AnimationCacheState {
static let shared = AnimationCacheState()
@ -118,45 +171,8 @@ private final class AnimationCacheState {
let cachePath = task.cachePath
let queue = self.queue
Queue.concurrentDefaultQueue().async { [weak self, weak task] in
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
if let lottieAnimation = LottieAnimation(data: decompressedData) {
let animationContainer = LottieAnimationContainer(animation: lottieAnimation)
let startTime = CFAbsoluteTimeGetCurrent()
let buffer = WriteBuffer()
var frameMapping = SerializedFrameMapping()
frameMapping.size = animationContainer.animation.size
frameMapping.frameCount = animationContainer.animation.frameCount
frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond
for i in 0 ..< frameMapping.frameCount {
frameMapping.frameRanges[i] = 0 ..< 1
}
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
for i in 0 ..< animationContainer.animation.frameCount {
animationContainer.update(i)
let frameRangeStart = buffer.length
if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) {
serializeNode(buffer: buffer, node: node)
let frameRangeEnd = buffer.length
frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd
}
}
let previousLength = buffer.length
buffer.length = 0
serializeFrameMapping(buffer: buffer, frameMapping: frameMapping)
buffer.length = previousLength
buffer.trim()
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
let zippedData = TGGZipData(buffer.data, 1.0)
print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic)
}
if let zippedData = cacheLottieMetalAnimation(path: path) {
let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic)
}
queue.async {
@ -191,12 +207,371 @@ private final class AnimationCacheState {
}
}
private func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
return transform
}
private final class RenderFrameState {
let canvasSize: CGSize
let frameState: PathFrameState
let currentBezierIndicesBuffer: PathRenderBuffer
let currentBuffer: PathRenderBuffer
var transform: CATransform3D
init(
canvasSize: CGSize,
frameState: PathFrameState,
currentBezierIndicesBuffer: PathRenderBuffer,
currentBuffer: PathRenderBuffer
) {
self.canvasSize = canvasSize
self.frameState = frameState
self.currentBezierIndicesBuffer = currentBezierIndicesBuffer
self.currentBuffer = currentBuffer
self.transform = defaultTransformForSize(canvasSize)
}
var transformStack: [CATransform3D] = []
func saveState() {
transformStack.append(transform)
}
func restoreState() {
transform = transformStack.removeLast()
}
func concat(_ other: CATransform3D) {
transform = CATransform3DConcat(other, transform)
}
private func fillPath(path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
fillState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
fillState.close()
@unknown default:
break
}
}
fillState.close()
self.frameState.add(fill: fillState)
}
private func strokePath(path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
strokeState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
strokeState.close()
@unknown default:
break
}
}
strokeState.complete()
self.frameState.add(stroke: strokeState)
}
func renderNodeContent(item: LottieRenderContent, alpha: Double) {
if let fill = item.fill {
if let solidShading = fill.shading as? LottieRenderContentSolidShading {
self.fillPath(
path: item.path,
shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
rule: fill.fillRule,
transform: transform
)
} else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
let gradientType: PathShading.Gradient.GradientType
switch gradientShading.gradientType {
case .linear:
gradientType = .linear
case .radial:
gradientType = .radial
@unknown default:
gradientType = .linear
}
var colorStops: [PathShading.Gradient.ColorStop] = []
for colorStop in gradientShading.colorStops {
colorStops.append(PathShading.Gradient.ColorStop(
color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
location: Float(colorStop.location)
))
}
let gradientShading = PathShading.Gradient(
gradientType: gradientType,
colorStops: colorStops,
start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
)
self.fillPath(
path: item.path,
shading: .gradient(gradientShading),
rule: fill.fillRule,
transform: transform
)
}
} else if let stroke = item.stroke {
if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
let color = solidShading.color
strokePath(
path: item.path,
width: stroke.lineWidth,
join: stroke.lineJoin,
cap: stroke.lineCap,
miterLimit: stroke.miterLimit,
color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
transform: transform
)
}
}
}
func renderNode(node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
let normalizedOpacity = node.opacity
let layerAlpha = normalizedOpacity * parentAlpha
if node.isHidden || normalizedOpacity == 0.0 {
return
}
saveState()
var needsTempContext = false
if node.mask != nil {
needsTempContext = true
} else {
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
}
var maskSurface: PathFrameState.MaskSurface?
if needsTempContext {
if node.mask != nil || node.masksToBounds {
var maskMode: PathFrameState.MaskSurface.Mode = .regular
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
if node.masksToBounds {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
fillState.close()
frameState.add(fill: fillState)
}
if let maskNode = node.mask {
if maskNode.isInvertedMatte {
maskMode = .inverse
}
renderNode(node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
}
restoreState()
maskSurface = frameState.popOffscreenMask(mode: maskMode)
}
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
} else {
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
}
var renderAlpha: CGFloat = 1.0
if needsTempContext {
renderAlpha = 1.0
} else {
renderAlpha = layerAlpha
}
if let renderContent = node.renderContent {
renderNodeContent(item: renderContent, alpha: renderAlpha)
}
for subnode in node.subnodes {
renderNode(node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
}
if needsTempContext {
restoreState()
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
concat(CATransform3DInvert(node.globalTransform))
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
}
restoreState()
}
func renderNode(animationContainer: LottieAnimationContainer, node: LottieRenderNodeProxy, globalSize: CGSize, parentAlpha: CGFloat) {
let normalizedOpacity = node.layer.opacity
let layerAlpha = normalizedOpacity * parentAlpha
if node.layer.isHidden || normalizedOpacity == 0.0 {
return
}
saveState()
var needsTempContext = false
if node.maskId != 0 {
needsTempContext = true
} else {
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.layer.masksToBounds
}
var maskSurface: PathFrameState.MaskSurface?
if needsTempContext {
if node.maskId != 0 || node.layer.masksToBounds {
var maskMode: PathFrameState.MaskSurface.Mode = .regular
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
if node.layer.masksToBounds {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
fillState.begin(point: SIMD2<Float>(Float(node.layer.bounds.minX), Float(node.layer.bounds.minY)))
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.minX), Float(node.layer.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.maxX), Float(node.layer.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.layer.bounds.maxX), Float(node.layer.bounds.minY)))
fillState.close()
frameState.add(fill: fillState)
}
if node.maskId != 0 {
let maskNode = animationContainer.getRenderNodeProxy(byId: node.maskId)
if maskNode.isInvertedMatte {
maskMode = .inverse
}
renderNode(animationContainer: animationContainer, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
}
restoreState()
maskSurface = frameState.popOffscreenMask(mode: maskMode)
}
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
} else {
concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0))
concat(node.layer.transform)
}
var renderAlpha: CGFloat = 1.0
if needsTempContext {
renderAlpha = 1.0
} else {
renderAlpha = layerAlpha
}
/*if let renderContent = node.renderContent {
renderNodeContent(item: renderContent, alpha: renderAlpha)
}*/
assert(false)
for i in 0 ..< node.subnodeCount {
let subnode = animationContainer.getRenderNodeSubnodeProxy(byId: node.internalId, index: i)
renderNode(animationContainer: animationContainer, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
}
if needsTempContext {
restoreState()
concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0))
concat(node.layer.transform)
concat(CATransform3DInvert(node.globalTransform))
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
}
restoreState()
}
}
public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject {
enum Content {
case serialized(frameMapping: SerializedFrameMapping, data: Data)
public enum Content {
case serialized(frameMapping: SerializedLottieMetalFrameMapping, data: Data)
case animation(LottieAnimationContainer)
var size: CGSize {
public var size: CGSize {
switch self {
case let .serialized(frameMapping, _):
return frameMapping.size
@ -205,7 +580,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
}
}
var frameCount: Int {
public var frameCount: Int {
switch self {
case let .serialized(frameMapping, _):
return frameMapping.frameCount
@ -214,7 +589,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
}
}
var framesPerSecond: Int {
public var framesPerSecond: Int {
switch self {
case let .serialized(frameMapping, _):
return frameMapping.framesPerSecond
@ -288,7 +663,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
}
}
init(content: Content) {
public init(content: Content) {
self.content = content
super.init()
@ -312,71 +687,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
fatalError("init(coder:) has not been implemented")
}
private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
fillState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
fillState.close()
@unknown default:
break
}
}
fillState.close()
frameState.add(fill: fillState)
}
private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
strokeState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
strokeState.close()
@unknown default:
break
}
}
strokeState.complete()
frameState.add(stroke: strokeState)
}
private var renderNodeCache: [Int: LottieRenderNode] = [:]
public func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
@ -392,196 +703,32 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
return
}
guard let node = content.updateAndGetRenderNode(frameIndex: self.frameIndex) else {
var maybeNode: LottieRenderNode?
if let current = self.renderNodeCache[self.frameIndex] {
maybeNode = current
} else {
if let value = content.updateAndGetRenderNode(frameIndex: self.frameIndex) {
maybeNode = value
//self.renderNodeCache[self.frameIndex] = value
}
}
guard let node = maybeNode else {
return
}
func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
return transform
}
let canvasSize = size
var transform = defaultTransformForSize(canvasSize)
concat(CATransform3DMakeScale(canvasSize.width / content.size.width, canvasSize.height / content.size.height, 1.0))
var transformStack: [CATransform3D] = []
func saveState() {
transformStack.append(transform)
}
func restoreState() {
transform = transformStack.removeLast()
}
func concat(_ other: CATransform3D) {
transform = CATransform3DConcat(other, transform)
}
func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) {
if let fill = item.fill {
if let solidShading = fill.shading as? LottieRenderContentSolidShading {
self.fillPath(
frameState: frameState,
path: item.path,
shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
rule: fill.fillRule,
transform: transform
)
} else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
let gradientType: PathShading.Gradient.GradientType
switch gradientShading.gradientType {
case .linear:
gradientType = .linear
case .radial:
gradientType = .radial
@unknown default:
gradientType = .linear
}
var colorStops: [PathShading.Gradient.ColorStop] = []
for colorStop in gradientShading.colorStops {
colorStops.append(PathShading.Gradient.ColorStop(
color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
location: Float(colorStop.location)
))
}
let gradientShading = PathShading.Gradient(
gradientType: gradientType,
colorStops: colorStops,
start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
)
self.fillPath(
frameState: frameState,
path: item.path,
shading: .gradient(gradientShading),
rule: fill.fillRule,
transform: transform
)
}
} else if let stroke = item.stroke {
if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
let color = solidShading.color
strokePath(
frameState: frameState,
path: item.path,
width: stroke.lineWidth,
join: stroke.lineJoin,
cap: stroke.lineCap,
miterLimit: stroke.miterLimit,
color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
transform: transform
)
}
}
}
func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
let normalizedOpacity = node.opacity
let layerAlpha = normalizedOpacity * parentAlpha
if node.isHidden || normalizedOpacity == 0.0 {
return
}
saveState()
var needsTempContext = false
if node.mask != nil {
needsTempContext = true
} else {
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
}
var maskSurface: PathFrameState.MaskSurface?
if needsTempContext {
if node.mask != nil || node.masksToBounds {
var maskMode: PathFrameState.MaskSurface.Mode = .regular
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
if node.masksToBounds {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
fillState.close()
frameState.add(fill: fillState)
}
if let maskNode = node.mask {
if maskNode.isInvertedMatte {
maskMode = .inverse
}
renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
}
restoreState()
maskSurface = frameState.popOffscreenMask(mode: maskMode)
}
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
} else {
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
}
var renderAlpha: CGFloat = 1.0
if needsTempContext {
renderAlpha = 1.0
} else {
renderAlpha = layerAlpha
}
if let renderContent = node.renderContent {
renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha)
}
for subnode in node.subnodes {
renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
}
if needsTempContext {
restoreState()
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
concat(CATransform3DInvert(node.globalTransform))
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
}
restoreState()
}
self.currentBuffer.reset()
self.currentBezierIndicesBuffer.reset()
let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: self.msaaSampleCount, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer)
renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0)
let frameContext = RenderFrameState(
canvasSize: size,
frameState: frameState,
currentBezierIndicesBuffer: self.currentBezierIndicesBuffer,
currentBuffer: self.currentBuffer
)
frameContext.concat(CATransform3DMakeScale(frameContext.canvasSize.width / content.size.width, frameContext.canvasSize.height / content.size.height, 1.0))
frameContext.renderNode(node: node, globalSize: frameContext.canvasSize, parentAlpha: 1.0)
final class ComputeOutput {
let pathRenderContext: PathRenderContext
@ -693,7 +840,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
self.offscreenHeap = offscreenHeap
}
frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize)
frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: frameContext.canvasSize)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
self.multisampleTextureQueue.append(multisampleTexture)
@ -701,7 +848,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje
return nil
}
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize)
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: frameContext.canvasSize)
renderEncoder.endEncoding()
@ -866,15 +1013,15 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
return
}
var serializedFrames: (SerializedFrameMapping, Data)?
var serializedFrames: (SerializedLottieMetalFrameMapping, Data)?
var cachePathValue: String?
if let cachePathPrefix {
let cachePath = cachePathPrefix + "-metal1"
cachePathValue = cachePath
if let data = try? Data(contentsOf: URL(fileURLWithPath: cachePath), options: .mappedIfSafe) {
if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) {
let serializedFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
serializedFrames = (serializedFrameMapping, unzippedData)
let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData))
serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData)
}
}
}

View File

@ -484,14 +484,14 @@ func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode {
)
}
struct SerializedFrameMapping {
public struct SerializedLottieMetalFrameMapping {
var size: CGSize = CGSize()
var frameCount: Int = 0
var framesPerSecond: Int = 0
var frameRanges: [Int: Range<Int>] = [:]
}
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMapping) {
func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedLottieMetalFrameMapping) {
buffer.write(size: frameMapping.size)
buffer.write(uInt32: UInt32(frameMapping.frameCount))
buffer.write(uInt32: UInt32(frameMapping.framesPerSecond))
@ -502,8 +502,8 @@ func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMap
}
}
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedFrameMapping {
var frameMapping = SerializedFrameMapping()
func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedLottieMetalFrameMapping {
var frameMapping = SerializedLottieMetalFrameMapping()
frameMapping.size = buffer.readSize()
frameMapping.frameCount = Int(buffer.readUInt32())

View File

@ -4450,7 +4450,7 @@ public final class StoryItemSetContainerComponent: Component {
context: component.context,
animationCache: component.context.animationCache,
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme),
items: reactionItems.map(ReactionContextItem.reaction),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
title: self.displayLikeReactions ? nil : (isGroup ? component.strings.Story_SendReactionAsGroupMessage : component.strings.Story_SendReactionAsMessage),
reactionsLocked: false,

View File

@ -111,7 +111,7 @@ extension ChatControllerImpl {
actions.animationCache = self.controllerInteraction?.presentationContext.animationCache
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
actions.reactionItems = topReactions.map { ReactionContextItem.reaction(item: $0, icon: .none) }
actions.selectedReactionItems = selectedReactions.reactions
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
if self.presentationInterfaceState.isPremium {
@ -131,7 +131,7 @@ extension ChatControllerImpl {
if !actions.reactionItems.isEmpty {
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
switch item {
case let .reaction(reaction):
case let .reaction(reaction, _):
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
default:
return nil

View File

@ -45,11 +45,22 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
effectItems = .single(nil)
}
let availableMessageEffects = selfController.context.availableMessageEffects |> take(1)
let hasPremium = selfController.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfController.context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
let _ = (combineLatest(
selfController.context.account.viewTracker.peerView(peerId) |> take(1),
effectItems
effectItems,
availableMessageEffects,
hasPremium
)
|> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems in
|> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium in
guard let selfController, let peer = peerViewMainPeer(peerView) else {
return
}
@ -98,7 +109,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
return
}
selfController.controllerInteraction?.scheduleCurrentMessage()
}, reactionItems: effectItems)
}, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium)
selfController.sendMessageActionsController = controller
if layout.isNonExclusive {
selfController.present(controller, in: .window(.root))

View File

@ -59,6 +59,8 @@ public protocol WallpaperBubbleBackgroundNode: ASDisplayNode {
func update(rect: CGRect, within containerSize: CGSize, animator: ControlledTransitionAnimator)
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double)
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat)
func reloadBindings()
}
public enum WallpaperDisplayMode {
@ -323,7 +325,7 @@ private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOver
}
}
final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode {
public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode {
final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
var implicitContentUpdate: Bool = true
@ -631,6 +633,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
gradientWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true)
}
}
func reloadBindings() {
}
}
final class BubbleBackgroundPortalNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode {
@ -679,6 +684,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
}
func reloadBindings() {
self.portalView.reloadPortal()
}
}
private final class BubbleBackgroundNodeReference {
@ -812,7 +821,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
}
var rotation: CGFloat = 0.0 {
public var rotation: CGFloat = 0.0 {
didSet {
var fromValue: CGFloat = 0.0
if let value = (self.layer.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue {
@ -845,7 +854,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
private static var cachedSharedPattern: (PatternKey, UIImage)?
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
var isReady: Signal<Bool, NoError> {
public var isReady: Signal<Bool, NoError> {
return self._isReady.get()
}
@ -920,7 +929,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.dimLayer.opacity = dimAlpha
}
func update(wallpaper: TelegramWallpaper, animated: Bool) {
public func update(wallpaper: TelegramWallpaper, animated: Bool) {
if self.wallpaper == wallpaper {
return
}
@ -1074,7 +1083,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.updateDimming()
}
func _internalUpdateIsSettingUpWallpaper() {
public func _internalUpdateIsSettingUpWallpaper() {
self.isSettingUpWallpaper = true
}
@ -1301,7 +1310,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size))
}
func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = (size, displayMode)
@ -1357,7 +1366,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
private var isAnimating = false
private var isLooping = false
func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
guard !(self.isLooping && self.isAnimating) else {
return
}
@ -1373,7 +1382,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {})
}
func updateIsLooping(_ isLooping: Bool) {
public func updateIsLooping(_ isLooping: Bool) {
let wasLooping = self.isLooping
self.isLooping = isLooping
@ -1382,7 +1391,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
}
func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) {
public func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) {
if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners {
self.bubbleTheme = bubbleTheme
self.bubbleCorners = bubbleCorners
@ -1430,7 +1439,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
}
func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool {
public func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool {
guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else {
return false
}
@ -1474,13 +1483,18 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
return false
}
public func makeLegacyBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
node.updateContents()
return node
}
func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
public func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
if !self.hasBubbleBackground(for: type) {
return nil
}
#if true
var sourceView: PortalSourceView?
switch type {
case .free:
@ -1499,14 +1513,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
return node
}
#else
let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type)
node.updateContents()
return node
#endif
}
func makeFreeBackground() -> PortalView? {
public func makeFreeBackground() -> PortalView? {
if !self.hasBubbleBackground(for: .free) {
return nil
}
@ -1519,7 +1528,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
}
func hasExtraBubbleBackground() -> Bool {
public func hasExtraBubbleBackground() -> Bool {
var isInvertedGradient = false
switch self.wallpaper {
case let .file(file):
@ -1532,7 +1541,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
return isInvertedGradient
}
func makeDimmedNode() -> ASDisplayNode? {
public func makeDimmedNode() -> ASDisplayNode? {
if let gradientBackgroundNode = self.gradientBackgroundNode {
return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode)
} else {