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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)) 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, context: self.context,
animationCache: self.context.animationCache, animationCache: self.context.animationCache,
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
items: reactionItems.map(ReactionContextItem.reaction), items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: Set(), selectedItems: Set(),
title: nil, title: nil,
reactionsLocked: false, reactionsLocked: false,

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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