Added URL Auth support
22
Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bold@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bold@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 603 B |
BIN
Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 906 B |
22
Images.xcassets/Chat/Input/Text/FormatItalic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "italic@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "italic@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 696 B |
22
Images.xcassets/Chat/Input/Text/FormatLink.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "link@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "link@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 858 B |
BIN
Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
22
Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "markdown@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "markdown@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 573 B |
BIN
Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 831 B |
22
Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "strike@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "strike@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 688 B |
BIN
Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 974 B |
@ -24,7 +24,15 @@
|
||||
0913469C21883C3700846E49 /* InstantPageDetailsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0913469B21883C3700846E49 /* InstantPageDetailsItem.swift */; };
|
||||
091417F221EF4E5D00C8325A /* WallpaperGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091417F121EF4E5D00C8325A /* WallpaperGalleryController.swift */; };
|
||||
091417F421EF4F5F00C8325A /* WallpaperGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091417F321EF4F5F00C8325A /* WallpaperGalleryItem.swift */; };
|
||||
091954732294591B00E11046 /* AnimatedStickerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091954722294591B00E11046 /* AnimatedStickerNode.swift */; };
|
||||
09195475229474E900E11046 /* AnimatedStickerPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09195474229474E900E11046 /* AnimatedStickerPlayerManager.swift */; };
|
||||
091954772294752C00E11046 /* AnimatedStickerPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091954762294752C00E11046 /* AnimatedStickerPlayer.swift */; };
|
||||
091954792294754E00E11046 /* AnimatedStickerUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091954782294754E00E11046 /* AnimatedStickerUtils.swift */; };
|
||||
0919547B2294788200E11046 /* AnimatedStickerVideoCompositor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0919547A2294788200E11046 /* AnimatedStickerVideoCompositor.swift */; };
|
||||
091BEAB3214552D9003AEA30 /* Vision.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02DADBE2138D76F00116225 /* Vision.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
0921F5FF228B09D2001A13D7 /* GZip.m in Sources */ = {isa = PBXBuildFile; fileRef = 0921F5FC228B01B6001A13D7 /* GZip.m */; };
|
||||
0921F60B228C8765001A13D7 /* ItemListPlaceholderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0921F60A228C8765001A13D7 /* ItemListPlaceholderItem.swift */; };
|
||||
0921F60E228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0921F60D228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift */; };
|
||||
092F368D2154AAEA001A9F49 /* SFCompactRounded-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 092F368C2154AAE9001A9F49 /* SFCompactRounded-Semibold.otf */; };
|
||||
092F36902157AB46001A9F49 /* ItemListCallListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 092F368F2157AB46001A9F49 /* ItemListCallListItem.swift */; };
|
||||
09310D32213ED5FC0020033A /* anim_ungroup.json in Resources */ = {isa = PBXBuildFile; fileRef = 09310D1A213BC5DE0020033A /* anim_ungroup.json */; };
|
||||
@ -1218,6 +1226,16 @@
|
||||
0913469B21883C3700846E49 /* InstantPageDetailsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageDetailsItem.swift; sourceTree = "<group>"; };
|
||||
091417F121EF4E5D00C8325A /* WallpaperGalleryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperGalleryController.swift; sourceTree = "<group>"; };
|
||||
091417F321EF4F5F00C8325A /* WallpaperGalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperGalleryItem.swift; sourceTree = "<group>"; };
|
||||
09167E1B22973A14005734A7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
091954722294591B00E11046 /* AnimatedStickerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerNode.swift; sourceTree = "<group>"; };
|
||||
09195474229474E900E11046 /* AnimatedStickerPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerPlayerManager.swift; sourceTree = "<group>"; };
|
||||
091954762294752C00E11046 /* AnimatedStickerPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerPlayer.swift; sourceTree = "<group>"; };
|
||||
091954782294754E00E11046 /* AnimatedStickerUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerUtils.swift; sourceTree = "<group>"; };
|
||||
0919547A2294788200E11046 /* AnimatedStickerVideoCompositor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerVideoCompositor.swift; sourceTree = "<group>"; };
|
||||
0921F5FB228B01B6001A13D7 /* GZip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GZip.h; sourceTree = "<group>"; };
|
||||
0921F5FC228B01B6001A13D7 /* GZip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GZip.m; sourceTree = "<group>"; };
|
||||
0921F60A228C8765001A13D7 /* ItemListPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListPlaceholderItem.swift; sourceTree = "<group>"; };
|
||||
0921F60D228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageActionUrlAuthController.swift; sourceTree = "<group>"; };
|
||||
092F368C2154AAE9001A9F49 /* SFCompactRounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SFCompactRounded-Semibold.otf"; sourceTree = "<group>"; };
|
||||
092F368F2157AB46001A9F49 /* ItemListCallListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListCallListItem.swift; sourceTree = "<group>"; };
|
||||
09310D1A213BC5DE0020033A /* anim_ungroup.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_ungroup.json; sourceTree = "<group>"; };
|
||||
@ -2532,6 +2550,19 @@
|
||||
name = "Language Suggestion";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0919546D229458E900E11046 /* Animated Stickers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
091954722294591B00E11046 /* AnimatedStickerNode.swift */,
|
||||
09167E1B22973A14005734A7 /* README.md */,
|
||||
091954762294752C00E11046 /* AnimatedStickerPlayer.swift */,
|
||||
09195474229474E900E11046 /* AnimatedStickerPlayerManager.swift */,
|
||||
0919547A2294788200E11046 /* AnimatedStickerVideoCompositor.swift */,
|
||||
091954782294754E00E11046 /* AnimatedStickerUtils.swift */,
|
||||
);
|
||||
name = "Animated Stickers";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
092F368B2154AAD6001A9F49 /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -4504,6 +4535,7 @@
|
||||
D0D03AE61DECB0D200220C46 /* Audio Recorder */,
|
||||
D0F69DBC1D6B886C0046BCD6 /* Player */,
|
||||
D0EC6FF71EBA1DAE00EBF1C3 /* Calls */,
|
||||
0919546D229458E900E11046 /* Animated Stickers */,
|
||||
D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */,
|
||||
D0F69E9D1D6B8E240046BCD6 /* Resources */,
|
||||
D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */,
|
||||
@ -4739,6 +4771,7 @@
|
||||
D044A0FA20BDC40C00326FAC /* CachedChannelAdmins.swift */,
|
||||
D01848E721A03BDA00B6DEBD /* ChatSearchState.swift */,
|
||||
D06350AD2229A7F800FA2B32 /* InChatPrefetchManager.swift */,
|
||||
0921F60D228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift */,
|
||||
);
|
||||
name = Chat;
|
||||
sourceTree = "<group>";
|
||||
@ -4894,6 +4927,7 @@
|
||||
D0B2F76D2052B59F00D3BFB9 /* InviteContactsController.swift */,
|
||||
D0B2F76F2052B5A800D3BFB9 /* InviteContactsControllerNode.swift */,
|
||||
D0B2F7712052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift */,
|
||||
0921F60A228C8765001A13D7 /* ItemListPlaceholderItem.swift */,
|
||||
);
|
||||
name = Contacts;
|
||||
sourceTree = "<group>";
|
||||
@ -4981,6 +5015,8 @@
|
||||
09E4A800223AE1B30038140F /* PeerType.swift */,
|
||||
09E4A806223D4B860038140F /* AccountUtils.swift */,
|
||||
D099E21F229405BB00561B75 /* Weak.swift */,
|
||||
0921F5FB228B01B6001A13D7 /* GZip.h */,
|
||||
0921F5FC228B01B6001A13D7 /* GZip.m */,
|
||||
);
|
||||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -5720,6 +5756,7 @@
|
||||
09749BC921F1BBA1008FDDE9 /* CallFeedbackController.swift in Sources */,
|
||||
099529FA21DD8A3100805E13 /* NavigationBarSearchContentNode.swift in Sources */,
|
||||
D0AEAE272080D6970013176E /* PaneSearchBarNode.swift in Sources */,
|
||||
0921F60B228C8765001A13D7 /* ItemListPlaceholderItem.swift in Sources */,
|
||||
D0EC6D4F1EB9F58800EBF1C3 /* ChatListSearchItem.swift in Sources */,
|
||||
D0EC6D501EB9F58800EBF1C3 /* ChatListNodeEntries.swift in Sources */,
|
||||
D0EC6D511EB9F58800EBF1C3 /* ChatListViewTransition.swift in Sources */,
|
||||
@ -5728,6 +5765,7 @@
|
||||
D0EC6D531EB9F58800EBF1C3 /* ChatHistoryViewForLocation.swift in Sources */,
|
||||
D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */,
|
||||
D0EC6D541EB9F58800EBF1C3 /* ChatHistoryEntriesForView.swift in Sources */,
|
||||
0921F5FF228B09D2001A13D7 /* GZip.m in Sources */,
|
||||
D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */,
|
||||
D0EC6D551EB9F58800EBF1C3 /* PreparedChatHistoryViewTransition.swift in Sources */,
|
||||
D0EB41FB1F30E75000838FE6 /* LegacyImageDownloadActor.swift in Sources */,
|
||||
@ -5801,6 +5839,7 @@
|
||||
D0C12EB01F9A8D1300600BB2 /* ListMessageDateHeader.swift in Sources */,
|
||||
09B4EE5221A7CC3E00847FA6 /* SolidRoundedButtonNode.swift in Sources */,
|
||||
D0E9BA5D1F055A3300F079A4 /* STPBINRange.m in Sources */,
|
||||
091954792294754E00E11046 /* AnimatedStickerUtils.swift in Sources */,
|
||||
D0EC6D741EB9F58800EBF1C3 /* AuthorizationSequenceSignUpControllerNode.swift in Sources */,
|
||||
D0EC6D751EB9F58800EBF1C3 /* TelegramRootController.swift in Sources */,
|
||||
D0EC6D761EB9F58800EBF1C3 /* ChatListController.swift in Sources */,
|
||||
@ -5947,6 +5986,7 @@
|
||||
09DE2F292269D5E30045E975 /* PrivacyIntroControllerNode.swift in Sources */,
|
||||
D0F67FF01EE6B8A8000E5906 /* ChannelMembersSearchController.swift in Sources */,
|
||||
D0EC6DAF1EB9F58900EBF1C3 /* ChatInterfaceInputContexts.swift in Sources */,
|
||||
0921F60E228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift in Sources */,
|
||||
D0EC6DB01EB9F58900EBF1C3 /* ChatInterfaceInputContextPanels.swift in Sources */,
|
||||
D0EC6DB11EB9F58900EBF1C3 /* ChatInterfaceInputNodes.swift in Sources */,
|
||||
D0EC6DB21EB9F58900EBF1C3 /* ChatInterfaceTitlePanelNodes.swift in Sources */,
|
||||
@ -5993,6 +6033,7 @@
|
||||
D0EC6DC61EB9F58900EBF1C3 /* MultiplexedSoftwareVideoSourceManager.swift in Sources */,
|
||||
D0EC6DC71EB9F58900EBF1C3 /* SampleBufferPool.swift in Sources */,
|
||||
0962E67721B673AF00245FD9 /* Permission.swift in Sources */,
|
||||
091954772294752C00E11046 /* AnimatedStickerPlayer.swift in Sources */,
|
||||
D0B21B13220D6E8C003F741D /* ActionSheetPeerItem.swift in Sources */,
|
||||
0900678F21ED8E0E00530762 /* HexColor.swift in Sources */,
|
||||
D0EC6DC81EB9F58900EBF1C3 /* MultiplexedVideoNode.swift in Sources */,
|
||||
@ -6122,6 +6163,7 @@
|
||||
D0EC6E041EB9F58900EBF1C3 /* SecretMediaPreviewController.swift in Sources */,
|
||||
09F2158D225CF5BC00AEDF6D /* Pasteboard.swift in Sources */,
|
||||
D0C26D571FDF2388004ABF18 /* OpenChatMessage.swift in Sources */,
|
||||
0919547B2294788200E11046 /* AnimatedStickerVideoCompositor.swift in Sources */,
|
||||
D0FA08BE20481EA300DD23FC /* Locale.swift in Sources */,
|
||||
D0E412CE206A707400BEE4A2 /* FormControllerTextItem.swift in Sources */,
|
||||
D007019C2029E8F2006B9E34 /* LegacyICloudFileController.swift in Sources */,
|
||||
@ -6342,6 +6384,7 @@
|
||||
D056CD741FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift in Sources */,
|
||||
D0F0AAE41EC21AAA005EE2A5 /* CallControllerButtonsNode.swift in Sources */,
|
||||
D0EC6E7A1EB9F58900EBF1C3 /* DebugController.swift in Sources */,
|
||||
091954732294591B00E11046 /* AnimatedStickerNode.swift in Sources */,
|
||||
D07ABBAB202A1BD1003671DE /* LegacyWallpaperEditor.swift in Sources */,
|
||||
09E2D9F1226F214000EA0AA4 /* EmojiResources.swift in Sources */,
|
||||
D0EC6E7B1EB9F58900EBF1C3 /* DebugAccountsController.swift in Sources */,
|
||||
@ -6356,6 +6399,7 @@
|
||||
D0EC6E7D1EB9F58900EBF1C3 /* ChangePhoneNumberIntroController.swift in Sources */,
|
||||
09F215AB2264ABA600AEDF6D /* PasscodeBackground.swift in Sources */,
|
||||
D0EC6E7E1EB9F58900EBF1C3 /* ChangePhoneNumberController.swift in Sources */,
|
||||
09195475229474E900E11046 /* AnimatedStickerPlayerManager.swift in Sources */,
|
||||
D0B21B17220D85E7003F741D /* TabBarAccountSwitchControllerNode.swift in Sources */,
|
||||
D0EC6E7F1EB9F58900EBF1C3 /* ChangePhoneNumberControllerNode.swift in Sources */,
|
||||
D0EC6E801EB9F58900EBF1C3 /* ChangePhoneNumberCodeController.swift in Sources */,
|
||||
|
||||
12
TelegramUI/AnimatedStickerNode.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
|
||||
final class AnimatedStickerNode: ASDisplayNode {
|
||||
|
||||
}
|
||||
5
TelegramUI/AnimatedStickerPlayer.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import UIKit
|
||||
|
||||
class AnimatedStickerPlayer: NSObject {
|
||||
|
||||
}
|
||||
5
TelegramUI/AnimatedStickerPlayerManager.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
final class AnimatedStickerPlayerManager {
|
||||
|
||||
}
|
||||
154
TelegramUI/AnimatedStickerUtils.swift
Normal file
@ -0,0 +1,154 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import AVFoundation
|
||||
import Lottie
|
||||
import TelegramUIPrivateModule
|
||||
|
||||
private func verifyLottieItems(_ items: [Any]?, shapes: Bool = true) -> Bool {
|
||||
if let items = items {
|
||||
for case let item as [AnyHashable: Any] in items {
|
||||
if let type = item["ty"] as? String {
|
||||
if type == "rp" || type == "sr" || type == "mm" || type == "gs" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if shapes, let subitems = item["it"] as? [Any] {
|
||||
if !verifyLottieItems(subitems, shapes: false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private func verifyLottieLayers(_ layers: [AnyHashable: Any]?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func validateStickerComposition(json: [AnyHashable: Any]) -> Bool {
|
||||
guard let tgs = json["tgs"] as? Int, tgs == 1 else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func convertCompressedLottieToCombinedMp4(data: Data, size: CGSize) -> Signal<String, NoError> {
|
||||
return Signal({ subscriber in
|
||||
let startTime = CACurrentMediaTime()
|
||||
let decompressedData = TGGUnzipData(data)
|
||||
if let decompressedData = decompressedData, let json = (try? JSONSerialization.jsonObject(with: decompressedData, options: [])) as? [AnyHashable: Any] {
|
||||
if let _ = json["tgs"] {
|
||||
let model = LOTComposition(json: json)
|
||||
if let startFrame = model.startFrame?.int32Value, let endFrame = model.endFrame?.int32Value {
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
let path = NSTemporaryDirectory() + "\(randomId).mp4"
|
||||
let url = URL(fileURLWithPath: path)
|
||||
|
||||
let videoSize = CGSize(width: size.width, height: size.height * 2.0)
|
||||
let scale = size.width / 512.0
|
||||
|
||||
if let assetWriter = try? AVAssetWriter(outputURL: url, fileType: AVFileType.mp4) {
|
||||
let videoSettings: [String: AnyObject] = [AVVideoCodecKey : AVVideoCodecH264 as AnyObject, AVVideoWidthKey : videoSize.width as AnyObject, AVVideoHeightKey : videoSize.height as AnyObject]
|
||||
|
||||
let assetWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
|
||||
let sourceBufferAttributes = [(kCVPixelBufferPixelFormatTypeKey as String): Int(kCVPixelFormatType_32ARGB),
|
||||
(kCVPixelBufferWidthKey as String): Float(videoSize.width),
|
||||
(kCVPixelBufferHeightKey as String): Float(videoSize.height)] as [String : Any]
|
||||
let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterInput, sourcePixelBufferAttributes: sourceBufferAttributes)
|
||||
|
||||
assetWriter.add(assetWriterInput)
|
||||
|
||||
if assetWriter.startWriting() {
|
||||
print("startedWriting at \(CACurrentMediaTime() - startTime)")
|
||||
assetWriter.startSession(atSourceTime: kCMTimeZero)
|
||||
|
||||
var currentFrame: Int32 = 0
|
||||
let writeQueue = DispatchQueue(label: "assetWriterQueue")
|
||||
writeQueue.async {
|
||||
let container = LOTAnimationLayerContainer(model: model, size: size)
|
||||
|
||||
let singleContext = DrawingContext(size: size, scale: 1.0, clear: true)
|
||||
let context = DrawingContext(size: size, scale: 1.0, clear: false)
|
||||
|
||||
let fps: Int32 = model.framerate?.int32Value ?? 30
|
||||
let frameDuration = CMTimeMake(1, fps)
|
||||
|
||||
assetWriterInput.requestMediaDataWhenReady(on: writeQueue) {
|
||||
while assetWriterInput.isReadyForMoreMediaData && startFrame + currentFrame < endFrame {
|
||||
let lastFrameTime = CMTimeMake(Int64(currentFrame - startFrame), fps)
|
||||
let presentationTime = currentFrame == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration)
|
||||
|
||||
singleContext.withContext { context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.saveGState()
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
container?.renderFrame(startFrame + currentFrame, in: context)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
let image = singleContext.generateImage()
|
||||
let alphaImage = generateTintedImage(image: image, color: .white, backgroundColor: .black)
|
||||
context.withFlippedContext { context in
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height), size: videoSize))
|
||||
if let image = image?.cgImage {
|
||||
context.draw(image, in: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: size))
|
||||
}
|
||||
if let alphaImage = alphaImage?.cgImage {
|
||||
context.draw(alphaImage, in: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
if let image = context.generateImage() {
|
||||
if let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool {
|
||||
let pixelBufferPointer = UnsafeMutablePointer<CVPixelBuffer?>.allocate(capacity: 1)
|
||||
let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, pixelBufferPointer)
|
||||
if let pixelBuffer = pixelBufferPointer.pointee, status == 0 {
|
||||
fillPixelBufferFromImage(image, pixelBuffer: pixelBuffer)
|
||||
|
||||
pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime)
|
||||
pixelBufferPointer.deinitialize(count: 1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
pixelBufferPointer.deallocate()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
currentFrame += 1
|
||||
}
|
||||
|
||||
if startFrame + currentFrame == endFrame {
|
||||
assetWriterInput.markAsFinished()
|
||||
assetWriter.finishWriting {
|
||||
subscriber.putNext(path)
|
||||
subscriber.putCompletion()
|
||||
print("animation render time \(CACurrentMediaTime() - startTime)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EmptyDisposable
|
||||
})
|
||||
}
|
||||
|
||||
private func fillPixelBufferFromImage(_ image: UIImage, pixelBuffer: CVPixelBuffer) {
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CGContext(data: pixelData, width: Int(image.size.width), height: Int(image.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
|
||||
context?.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
}
|
||||
5
TelegramUI/AnimatedStickerVideoCompositor.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import AVFoundation
|
||||
|
||||
final class AnimatedStickerVideoCompositor: NSObject {
|
||||
|
||||
}
|
||||
@ -213,3 +213,17 @@ final class CachedEmojiRepresentation: CachedMediaResourceRepresentation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepresentation {
|
||||
var uniqueId: String {
|
||||
return "animated-sticker"
|
||||
}
|
||||
|
||||
func isEqual(to: CachedMediaResourceRepresentation) -> Bool {
|
||||
if let _ = to as? CachedAnimatedStickerRepresentation {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
func setFile(context: AccountContext, fileReference: FileMediaReference) {
|
||||
if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) {
|
||||
let signal = chatMessageAnimationData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false)
|
||||
let signal = chatMessageAnimatedStrickerBackingData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false)
|
||||
|> mapToSignal { data, completed -> Signal<Data, NoError> in
|
||||
if completed, let data = data {
|
||||
return .single(data)
|
||||
|
||||
@ -160,20 +160,20 @@ final class ChatButtonKeyboardInputNode: ChatInputNode {
|
||||
if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button {
|
||||
switch markupButton.action {
|
||||
case .text:
|
||||
controllerInteraction.sendMessage(markupButton.title)
|
||||
self.controllerInteraction.sendMessage(markupButton.title)
|
||||
case let .url(url):
|
||||
controllerInteraction.openUrl(url, true, nil)
|
||||
self.controllerInteraction.openUrl(url, true, nil)
|
||||
case .requestMap:
|
||||
controllerInteraction.shareCurrentLocation()
|
||||
self.controllerInteraction.shareCurrentLocation()
|
||||
case .requestPhone:
|
||||
controllerInteraction.shareAccountContact()
|
||||
self.controllerInteraction.shareAccountContact()
|
||||
case .openWebApp:
|
||||
if let message = self.message {
|
||||
controllerInteraction.requestMessageActionCallback(message.id, nil, true)
|
||||
self.controllerInteraction.requestMessageActionCallback(message.id, nil, true)
|
||||
}
|
||||
case let .callback(data):
|
||||
if let message = self.message {
|
||||
controllerInteraction.requestMessageActionCallback(message.id, data, false)
|
||||
self.controllerInteraction.requestMessageActionCallback(message.id, data, false)
|
||||
}
|
||||
case let .switchInline(samePeer, query):
|
||||
if let message = message {
|
||||
@ -195,13 +195,15 @@ final class ChatButtonKeyboardInputNode: ChatInputNode {
|
||||
peerId = message.id.peerId
|
||||
}
|
||||
if let botPeer = botPeer, let addressName = botPeer.addressName {
|
||||
controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil)
|
||||
self.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil)
|
||||
}
|
||||
}
|
||||
case .payment:
|
||||
break
|
||||
case .urlAuth:
|
||||
break
|
||||
case let .urlAuth(url, buttonId):
|
||||
if let message = self.message {
|
||||
self.controllerInteraction.requestMessageActionUrlAuth(url, message.id, buttonId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +128,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
private let sentMessageEventsDisposable = MetaDisposable()
|
||||
private let failedMessageEventsDisposable = MetaDisposable()
|
||||
private let messageActionCallbackDisposable = MetaDisposable()
|
||||
private let messageActionUrlAuthDisposable = MetaDisposable()
|
||||
private let editMessageDisposable = MetaDisposable()
|
||||
private let enqueueMediaMessageDisposable = MetaDisposable()
|
||||
private var resolvePeerByNameDisposable: MetaDisposable?
|
||||
@ -643,6 +644,121 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, requestMessageActionUrlAuth: { [weak self] defaultUrl, messageId, buttonId in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if !$0.contains(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.append(.requestInProgress)
|
||||
return updatedContexts.sorted()
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), requestMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId) |> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.index(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})) |> deliverOnMainQueue).start(next: { peer, result in
|
||||
if let strongSelf = self {
|
||||
switch result {
|
||||
case .default:
|
||||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||||
case let .request(domain, bot, requestWriteAccess):
|
||||
let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: peer.displayTitle, open: { [weak self] authorize, allowWriteAccess in
|
||||
if let strongSelf = self {
|
||||
if authorize {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if !$0.contains(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.append(.requestInProgress)
|
||||
return updatedContexts.sorted()
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.messageActionUrlAuthDisposable.set(((acceptMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId, allowWriteAccess: allowWriteAccess) |> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.index(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
switch result {
|
||||
case let .accepted(url):
|
||||
strongSelf.openUrl(url, concealed: false)
|
||||
default:
|
||||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||||
}
|
||||
}
|
||||
})
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
case let .accepted(url):
|
||||
strongSelf.openUrl(url, concealed: false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, activateSwitchInline: { [weak self] peerId, inputString in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1706,6 +1822,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
self.sentMessageEventsDisposable.dispose()
|
||||
self.failedMessageEventsDisposable.dispose()
|
||||
self.messageActionCallbackDisposable.dispose()
|
||||
self.messageActionUrlAuthDisposable.dispose()
|
||||
self.editMessageDisposable.dispose()
|
||||
self.enqueueMediaMessageDisposable.dispose()
|
||||
self.resolvePeerByNameDisposable?.dispose()
|
||||
|
||||
@ -62,6 +62,7 @@ public final class ChatControllerInteraction {
|
||||
let sendSticker: (FileMediaReference, Bool) -> Void
|
||||
let sendGif: (FileMediaReference) -> Void
|
||||
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void
|
||||
let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void
|
||||
let activateSwitchInline: (PeerId?, String) -> Void
|
||||
let openUrl: (String, Bool, Bool?) -> Void
|
||||
let shareCurrentLocation: () -> Void
|
||||
@ -102,7 +103,7 @@ public final class ChatControllerInteraction {
|
||||
var pollActionState: ChatInterfacePollActionState
|
||||
var searchTextHighightState: String?
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -114,6 +115,7 @@ public final class ChatControllerInteraction {
|
||||
self.sendSticker = sendSticker
|
||||
self.sendGif = sendGif
|
||||
self.requestMessageActionCallback = requestMessageActionCallback
|
||||
self.requestMessageActionUrlAuth = requestMessageActionUrlAuth
|
||||
self.activateSwitchInline = activateSwitchInline
|
||||
self.openUrl = openUrl
|
||||
self.shareCurrentLocation = shareCurrentLocation
|
||||
@ -153,7 +155,7 @@ public final class ChatControllerInteraction {
|
||||
|
||||
static var `default`: ChatControllerInteraction {
|
||||
return ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
|
||||
@ -79,7 +79,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
switch button.action {
|
||||
case .text:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage
|
||||
case .url:
|
||||
case .url, .urlAuth:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
case .requestPhone:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
|
||||
372
TelegramUI/ChatMessageActionUrlAuthController.swift
Normal file
@ -0,0 +1,372 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private final class ChatMessageActionUrlAuthContentActionNode: HighlightableButtonNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
let action: TextAlertAction
|
||||
|
||||
init(theme: AlertControllerTheme, action: TextAlertAction) {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.alpha = 0.0
|
||||
|
||||
self.action = action
|
||||
|
||||
super.init()
|
||||
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.highligthedChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
if value {
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.backgroundNode.alpha = 1.0
|
||||
} else if !strongSelf.backgroundNode.alpha.isZero {
|
||||
strongSelf.backgroundNode.alpha = 0.0
|
||||
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.backgroundNode.backgroundColor = theme.highlightedItemColor
|
||||
|
||||
var font = Font.regular(17.0)
|
||||
var color = theme.accentColor
|
||||
switch self.action.type {
|
||||
case .defaultAction, .genericAction:
|
||||
break
|
||||
case .destructiveAction:
|
||||
color = theme.destructiveColor
|
||||
}
|
||||
switch self.action.type {
|
||||
case .defaultAction:
|
||||
font = Font.semibold(17.0)
|
||||
case .destructiveAction, .genericAction:
|
||||
break
|
||||
}
|
||||
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func pressed() {
|
||||
self.action.action()
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let defaultUrl: String
|
||||
private let domain: String
|
||||
private let bot: Peer
|
||||
private let displayName: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
private let authorizeCheckNode: CheckNode
|
||||
private let authorizeLabelNode: ASTextNode
|
||||
private let allowWriteCheckNode: CheckNode
|
||||
private let allowWriteLabelNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [ChatMessageActionUrlAuthContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
var authorize: Bool = true {
|
||||
didSet {
|
||||
self.authorizeCheckNode.setIsChecked(self.authorize, animated: true)
|
||||
if !self.authorize && self.allowWriteAccess {
|
||||
self.allowWriteAccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var allowWriteAccess: Bool = true {
|
||||
didSet {
|
||||
self.allowWriteCheckNode.setIsChecked(self.allowWriteAccess, animated: true)
|
||||
if !self.authorize && self.allowWriteAccess {
|
||||
self.authorize = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, defaultUrl: String, domain: String, bot: Peer, requestWriteAccess: Bool, displayName: String, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
self.defaultUrl = defaultUrl
|
||||
self.domain = domain
|
||||
self.bot = bot
|
||||
self.displayName = displayName
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.authorizeCheckNode = CheckNode(strokeColor: theme.separatorColor, fillColor: theme.accentColor, foregroundColor: .white, style: .plain)
|
||||
self.authorizeCheckNode.setIsChecked(true, animated: false)
|
||||
self.authorizeLabelNode = ASTextNode()
|
||||
self.authorizeLabelNode.maximumNumberOfLines = 2
|
||||
|
||||
self.allowWriteCheckNode = CheckNode(strokeColor: theme.separatorColor, fillColor: theme.accentColor, foregroundColor: .white, style: .plain)
|
||||
self.allowWriteCheckNode.setIsChecked(true, animated: false)
|
||||
self.allowWriteLabelNode = ASTextNode()
|
||||
self.allowWriteLabelNode.maximumNumberOfLines = 2
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> ChatMessageActionUrlAuthContentActionNode in
|
||||
return ChatMessageActionUrlAuthContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.authorizeCheckNode)
|
||||
self.addSubnode(self.authorizeLabelNode)
|
||||
|
||||
if requestWriteAccess {
|
||||
self.addSubnode(self.allowWriteCheckNode)
|
||||
self.addSubnode(self.allowWriteLabelNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.authorizeCheckNode.addTarget(target: self, action: #selector(self.authorizePressed))
|
||||
self.allowWriteCheckNode.addTarget(target: self, action: #selector(self.allowWritePressed))
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
@objc private func authorizePressed() {
|
||||
self.authorize = !self.authorize
|
||||
}
|
||||
|
||||
@objc private func allowWritePressed() {
|
||||
self.allowWriteAccess = !self.allowWriteAccess
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Conversation_OpenBotLinkTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.textNode.attributedText = formattedText(strings.Conversation_OpenBotLinkText(self.defaultUrl).0, color: theme.primaryColor, textAlignment: .center)
|
||||
self.authorizeLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkLogin(self.domain, self.displayName).0, color: theme.primaryColor)
|
||||
self.allowWriteLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkAllowMessages(self.bot.displayTitle).0, color: theme.primaryColor)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 9.0
|
||||
|
||||
let textSize = self.textNode.measure(size)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 16.0
|
||||
|
||||
let checkSize = CGSize(width: 32.0, height: 32.0)
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
let authorizeSize = self.authorizeLabelNode.measure(condensedSize)
|
||||
transition.updateFrame(node: self.authorizeLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: authorizeSize))
|
||||
transition.updateFrame(node: self.authorizeCheckNode, frame: CGRect(origin: CGPoint(x: 7.0, y: origin.y - 7.0), size: checkSize))
|
||||
origin.y += authorizeSize.height
|
||||
entriesHeight += authorizeSize.height
|
||||
|
||||
if self.allowWriteLabelNode.supernode != nil {
|
||||
origin.y += 16.0
|
||||
entriesHeight += 16.0
|
||||
|
||||
let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize)
|
||||
transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize))
|
||||
transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 7.0, y: origin.y - 7.0), size: checkSize))
|
||||
origin.y += allowWriteSize.height
|
||||
entriesHeight += allowWriteSize.height
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 30.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
func chatMessageActionUrlAuthController(context: AccountContext, defaultUrl: String, domain: String, bot: Peer, requestWriteAccess: Bool, displayName: String, open: @escaping (Bool, Bool) -> Void) -> AlertController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
|
||||
var contentNode: ChatMessageActionUrlAuthAlertContentNode?
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_OpenBotLinkOpen, action: {
|
||||
dismissImpl?(true)
|
||||
if let contentNode = contentNode {
|
||||
open(contentNode.authorize, contentNode.allowWriteAccess)
|
||||
}
|
||||
})]
|
||||
contentNode = ChatMessageActionUrlAuthAlertContentNode(theme: AlertControllerTheme(presentationTheme: theme), ptheme: theme, strings: strings, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: displayName, actions: actions)
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
@ -4,64 +4,165 @@ import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Lottie
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
|
||||
private final class StickerAnimationNode : ASDisplayNode {
|
||||
private var disposable = MetaDisposable()
|
||||
var loopCount: Int = 0
|
||||
private class AlphaFrameFilter: CIFilter {
|
||||
static var kernel: CIColorKernel? = {
|
||||
return CIColorKernel(source: """
|
||||
kernel vec4 alphaFrame(__sample s, __sample m) {
|
||||
return vec4( s.rgb, m.r );
|
||||
}
|
||||
""")
|
||||
}()
|
||||
|
||||
var inputImage: CIImage?
|
||||
var maskImage: CIImage?
|
||||
|
||||
override var outputImage: CIImage? {
|
||||
let kernel = AlphaFrameFilter.kernel!
|
||||
guard let inputImage = inputImage, let maskImage = maskImage else {
|
||||
return nil
|
||||
}
|
||||
let args = [inputImage as AnyObject, maskImage as AnyObject]
|
||||
return kernel.apply(extent: inputImage.extent, arguments: args)
|
||||
}
|
||||
}
|
||||
|
||||
private func createVideoComposition(for playerItem: AVPlayerItem) -> AVVideoComposition? {
|
||||
let videoSize = CGSize(width: playerItem.presentationSize.width, height: playerItem.presentationSize.height / 2.0)
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
let composition = AVMutableVideoComposition(asset: playerItem.asset, applyingCIFiltersWithHandler: { request in
|
||||
let sourceRect = CGRect(origin: .zero, size: videoSize)
|
||||
let alphaRect = sourceRect.offsetBy(dx: 0, dy: sourceRect.height)
|
||||
let filter = AlphaFrameFilter()
|
||||
filter.inputImage = request.sourceImage.cropped(to: alphaRect)
|
||||
.transformed(by: CGAffineTransform(translationX: 0, y: -sourceRect.height))
|
||||
filter.maskImage = request.sourceImage.cropped(to: sourceRect)
|
||||
return request.finish(with: filter.outputImage!, context: nil)
|
||||
})
|
||||
composition.renderSize = videoSize
|
||||
return composition
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class StickerAnimationNode: ASDisplayNode {
|
||||
private var account: Account?
|
||||
private var fileReference: FileMediaReference?
|
||||
private let disposable = MetaDisposable()
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
var playerLayer: AVPlayerLayer {
|
||||
return self.layer as! AVPlayerLayer
|
||||
}
|
||||
|
||||
var player: AVPlayer? {
|
||||
get {
|
||||
if self.isNodeLoaded {
|
||||
return self.playerLayer.player
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
if let player = self.playerLayer.player {
|
||||
player.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate))
|
||||
}
|
||||
self.playerLayer.player = newValue
|
||||
if let newValue = newValue {
|
||||
newValue.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: [], context: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var playerItem: AVPlayerItem? = nil {
|
||||
willSet {
|
||||
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status))
|
||||
}
|
||||
didSet {
|
||||
self.playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: nil)
|
||||
self.setupLooping()
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
let view = LOTAnimationView()
|
||||
return view
|
||||
self.setLayerBlock({
|
||||
return AVPlayerLayer()
|
||||
})
|
||||
|
||||
self.playerLayer.isHidden = true
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
self.playerLayer.pixelBufferAttributes = [(kCVPixelBufferPixelFormatTypeKey as String): kCVPixelFormatType_32BGRA]
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self.didPlayToEndTimeObsever as Any)
|
||||
self.player = nil
|
||||
self.playerItem = nil
|
||||
self.disposable.dispose()
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
func setSignal(_ signal: Signal<Data, NoError>) {
|
||||
self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] next in
|
||||
if let object = try? JSONSerialization.jsonObject(with: next, options: []) as? [AnyHashable: Any], let json = object {
|
||||
self?.animationView()?.setAnimation(json: json)
|
||||
func setup(account: Account, fileReference: FileMediaReference) {
|
||||
self.disposable.set(chatMessageAnimationData(postbox: account.postbox, fileReference: fileReference, synchronousLoad: false).start(next: { [weak self] data in
|
||||
if let strongSelf = self, data.complete {
|
||||
let playerItem = AVPlayerItem(url: URL(fileURLWithPath: data.path))
|
||||
strongSelf.player = AVPlayer(playerItem: playerItem)
|
||||
strongSelf.playerItem = playerItem
|
||||
}
|
||||
}))
|
||||
self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start())
|
||||
}
|
||||
|
||||
func animationView() -> LOTAnimationView? {
|
||||
return self.view as? LOTAnimationView
|
||||
private func setupLooping() {
|
||||
guard let playerItem = self.playerItem, let player = self.player else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didPlayToEndTimeObsever = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil, using: { _ in
|
||||
player.seek(to: kCMTimeZero) { _ in
|
||||
player.play()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var didPlayToEndTimeObsever: NSObjectProtocol? = nil {
|
||||
willSet(newObserver) {
|
||||
if let observer = self.didPlayToEndTimeObsever, self.didPlayToEndTimeObsever !== newObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if let playerItem = object as? AVPlayerItem, playerItem === self.playerItem {
|
||||
if case .readyToPlay = playerItem.status, playerItem.videoComposition == nil {
|
||||
playerItem.videoComposition = createVideoComposition(for: playerItem)
|
||||
playerItem.seekingWaitsForVideoCompositionRendering = true
|
||||
}
|
||||
self.player?.play()
|
||||
} else if let player = object as? AVPlayer, player === self.player {
|
||||
if self.playerLayer.isHidden && player.rate > 0.0 {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.playerLayer.isHidden = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
func play() {
|
||||
DispatchQueue.main.async {
|
||||
if let animationView = self.animationView(), !animationView.isAnimationPlaying {
|
||||
self.loopCount = 2
|
||||
|
||||
var completion: ((Bool) -> Void)!
|
||||
let placeholder: (Bool) -> Void = { [weak animationView] _ in
|
||||
self.loopCount -= 1
|
||||
if self.loopCount > 0 {
|
||||
animationView?.play(completion: completion)
|
||||
}
|
||||
}
|
||||
completion = placeholder
|
||||
|
||||
if !animationView.isAnimationPlaying {
|
||||
animationView.play(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func reset() {
|
||||
DispatchQueue.main.async {
|
||||
if let animationView = self.animationView() {
|
||||
animationView.stop()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,8 +178,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
var telegramFile: TelegramMediaFile?
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var replyInfoNode: ChatMessageReplyInfoNode?
|
||||
private var replyBackgroundNode: ASImageNode?
|
||||
@ -104,10 +203,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
@ -141,19 +236,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
for media in item.message.media {
|
||||
if let telegramFile = media as? TelegramMediaFile {
|
||||
if self.telegramFile != telegramFile {
|
||||
let signal = chatMessageAnimationData(postbox: item.context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: telegramFile), synchronousLoad: false)
|
||||
|> mapToSignal { data, completed -> Signal<Data, NoError> in
|
||||
if completed, let data = data {
|
||||
return .single(data)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
self.telegramFile = telegramFile
|
||||
self.animationNode.setSignal(signal)
|
||||
self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
|
||||
|
||||
self.animationNode.play()
|
||||
self.animationNode.setup(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile))
|
||||
}
|
||||
|
||||
break
|
||||
@ -454,7 +539,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
if let item = self.item, self.imageNode.frame.contains(location) {
|
||||
self.animationNode.play()
|
||||
//self.animationNode.play()
|
||||
//let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
return
|
||||
}
|
||||
|
||||
@ -346,7 +346,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
||||
|
||||
loop: for media in self.message.media {
|
||||
if let telegramFile = media as? TelegramMediaFile {
|
||||
if GlobalExperimentalSettings.animatedStickers && telegramFile.fileName == "animation.json" {
|
||||
if let fileName = telegramFile.fileName, fileName.hasSuffix(".tgs"), let size = telegramFile.size, size > 0 && size < 64 * 1024 {
|
||||
viewClassName = ChatMessageAnimatedStickerItemNode.self
|
||||
break loop
|
||||
}
|
||||
|
||||
@ -749,8 +749,8 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
}
|
||||
case .payment:
|
||||
item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
|
||||
case .urlAuth:
|
||||
break
|
||||
case let .urlAuth(url, buttonId):
|
||||
item.controllerInteraction.requestMessageActionUrlAuth(url, item.message.id, buttonId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
self?.openPeerMention(name)
|
||||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame in
|
||||
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
|
||||
}, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in
|
||||
self?.openUrl(url)
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
@ -607,9 +607,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
if let query = strongSelf.filter.query, hasFilter {
|
||||
text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterQueryText(query).0
|
||||
} else {
|
||||
|
||||
text = isSupergroup ? strongSelf.presentationData.strings.Group_AdminLog_EmptyText : strongSelf.presentationData.strings.Broadcast_AdminLog_EmptyText
|
||||
|
||||
}
|
||||
strongSelf.emptyNode.setup(title: hasFilter ? strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterTitle : strongSelf.presentationData.strings.Channel_AdminLog_EmptyTitle, text: text)
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ private struct FontAttributes: OptionSet {
|
||||
static let bold = FontAttributes(rawValue: 1 << 0)
|
||||
static let italic = FontAttributes(rawValue: 1 << 1)
|
||||
static let monospace = FontAttributes(rawValue: 1 << 2)
|
||||
static let strikethrough = FontAttributes(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor) -> NSAttributedString {
|
||||
|
||||
@ -276,17 +276,18 @@ class ContactListActionItemNode: ListViewItemNode {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
strongSelf.bottomStripeNode.isHidden = hideBottomStripe
|
||||
if !hideBottomStripe {
|
||||
print("")
|
||||
}
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -245,6 +245,13 @@ public class ContactsController: ViewController {
|
||||
openPeer(peer, false)
|
||||
}
|
||||
|
||||
self.contactsNode.openPeopleNearby = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
//let controller = peopleNearbyController(context: strongSelf.context)
|
||||
//(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
|
||||
self.contactsNode.openInvite = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(InviteContactsController(context: strongSelf.context))
|
||||
|
||||
@ -17,6 +17,7 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
|
||||
var requestDeactivateSearch: (() -> Void)?
|
||||
var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)?
|
||||
var openPeopleNearby: (() -> Void)?
|
||||
var openInvite: (() -> Void)?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@ -27,7 +28,11 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var addNearbyImpl: (() -> Void)?
|
||||
var inviteImpl: (() -> Void)?
|
||||
//ContactListAdditionalOption(title: presentationData.strings.Contacts_AddPeopleNearby, icon: .generic(UIImage(bundleImageName: "Contact List/PeopleNearbyIcon")!), action: {
|
||||
// addNearbyImpl?()
|
||||
//}),
|
||||
let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
|
||||
inviteImpl?()
|
||||
})]
|
||||
@ -68,6 +73,12 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
}
|
||||
})
|
||||
|
||||
addNearbyImpl = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.openPeopleNearby?()
|
||||
}
|
||||
}
|
||||
|
||||
inviteImpl = { [weak self] in
|
||||
let _ = (DeviceAccess.authorizationStatus(context: context, subject: .contacts)
|
||||
|> take(1)
|
||||
|
||||
@ -570,7 +570,6 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
if let controller = controller {
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(c)
|
||||
@ -579,6 +578,6 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt
|
||||
presentControllerImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
}
|
||||
|
||||
func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? {
|
||||
var status = frame.packet.send(toDecoder: self.codecContext)
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
if self.codecContext.receive(into: self.videoFrame) {
|
||||
var pts = CMTimeMake(self.videoFrame.pts, frame.pts.timescale)
|
||||
|
||||
@ -8,6 +8,8 @@ import Display
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import WebP
|
||||
import Lottie
|
||||
import TelegramUIPrivateModule
|
||||
|
||||
public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
if let representation = representation as? CachedStickerAJpegRepresentation {
|
||||
@ -16,7 +18,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR
|
||||
if !data.complete {
|
||||
return .complete()
|
||||
}
|
||||
return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: data, representation: representation)
|
||||
return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: data, representation: representation)
|
||||
}
|
||||
} else if let representation = representation as? CachedScaledImageRepresentation {
|
||||
return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false))
|
||||
@ -105,6 +107,14 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR
|
||||
return fetchEmojiThumbnailRepresentation(account: account, resource: resource, representation: representation)
|
||||
} else if let representation = representation as? CachedEmojiRepresentation {
|
||||
return fetchEmojiRepresentation(account: account, resource: resource, representation: representation)
|
||||
} else if let representation = representation as? CachedAnimatedStickerRepresentation {
|
||||
return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false))
|
||||
|> mapToSignal { data -> Signal<CachedMediaResourceRepresentationResult, NoError> in
|
||||
if !data.complete {
|
||||
return .complete()
|
||||
}
|
||||
return fetchAnimatedStickerRepresentation(account: account, resource: resource, resourceData: data, representation: representation)
|
||||
}
|
||||
}
|
||||
return .never()
|
||||
}
|
||||
@ -871,3 +881,15 @@ private func fetchEmojiRepresentation(account: Account, resource: MediaResource,
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAnimatedStickerRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedAnimatedStickerRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal({ subscriber in
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) {
|
||||
return convertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: 400.0, height: 400.0)).start(next: { path in
|
||||
subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path))
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
}) |> runOn(Queue.concurrentDefaultQueue())
|
||||
}
|
||||
|
||||
17
TelegramUI/GZip.h
Normal file
@ -0,0 +1,17 @@
|
||||
#ifndef Telegram_GZip_h
|
||||
#define Telegram_GZip_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
NSData *TGGZipData(NSData *data, float level);
|
||||
NSData *TGGUnzipData(NSData *data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
79
TelegramUI/GZip.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "GZip.h"
|
||||
|
||||
#import <zlib.h>
|
||||
|
||||
bool TGIsGzippedData(NSData *data) {
|
||||
const UInt8 *bytes = (const UInt8 *)data.bytes;
|
||||
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
|
||||
}
|
||||
|
||||
NSData *TGGZipData(NSData *data, float level) {
|
||||
if (data.length == 0 || TGIsGzippedData(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
z_stream stream;
|
||||
stream.zalloc = Z_NULL;
|
||||
stream.zfree = Z_NULL;
|
||||
stream.opaque = Z_NULL;
|
||||
stream.avail_in = (uint)data.length;
|
||||
stream.next_in = (Bytef *)(void *)data.bytes;
|
||||
stream.total_out = 0;
|
||||
stream.avail_out = 0;
|
||||
|
||||
static const NSUInteger ChunkSize = 16384;
|
||||
|
||||
NSMutableData *output = nil;
|
||||
int compression = (level < 0.0f) ? Z_DEFAULT_COMPRESSION : (int)(roundf(level * 9));
|
||||
if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) {
|
||||
output = [NSMutableData dataWithLength:ChunkSize];
|
||||
while (stream.avail_out == 0) {
|
||||
if (stream.total_out >= output.length) {
|
||||
output.length += ChunkSize;
|
||||
}
|
||||
stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
|
||||
stream.avail_out = (uInt)(output.length - stream.total_out);
|
||||
deflate(&stream, Z_FINISH);
|
||||
}
|
||||
deflateEnd(&stream);
|
||||
output.length = stream.total_out;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
NSData *TGGUnzipData(NSData *data)
|
||||
{
|
||||
if (data.length == 0 || !TGIsGzippedData(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
z_stream stream;
|
||||
stream.zalloc = Z_NULL;
|
||||
stream.zfree = Z_NULL;
|
||||
stream.avail_in = (uint)data.length;
|
||||
stream.next_in = (Bytef *)data.bytes;
|
||||
stream.total_out = 0;
|
||||
stream.avail_out = 0;
|
||||
|
||||
NSMutableData *output = nil;
|
||||
if (inflateInit2(&stream, 47) == Z_OK) {
|
||||
int status = Z_OK;
|
||||
output = [NSMutableData dataWithCapacity:data.length * 2];
|
||||
while (status == Z_OK) {
|
||||
if (stream.total_out >= output.length) {
|
||||
output.length += data.length / 2;
|
||||
}
|
||||
stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
|
||||
stream.avail_out = (uInt)(output.length - stream.total_out);
|
||||
status = inflate (&stream, Z_SYNC_FLUSH);
|
||||
}
|
||||
if (inflateEnd(&stream) == Z_OK) {
|
||||
if (status == Z_STREAM_END) {
|
||||
output.length = stream.total_out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
205
TelegramUI/ItemListPlaceholderItem.swift
Normal file
@ -0,0 +1,205 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
class ItemListPlaceholderItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId, style: ItemListStyle, tag: ItemListItemTag? = nil) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListPlaceholderItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ItemListPlaceholderItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selectable = false
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
|
||||
let textNode: TextNode
|
||||
|
||||
private var item: ItemListPlaceholderItem?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListPlaceholderItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
let rightInset = 16.0 + params.rightInset
|
||||
|
||||
let textColor = item.theme.list.itemSecondaryTextColor
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let height: CGFloat = 34.0 + textLayout.size.height
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 17.0), size: textLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
@ -142,7 +142,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate {
|
||||
deinit {
|
||||
self.displayLink.invalidate()
|
||||
self.displayLink.isPaused = true
|
||||
for(_, disposable) in statusDisposable {
|
||||
for(_, disposable) in self.statusDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
for (_, value) in self.visibleLayers {
|
||||
|
||||
@ -55,8 +55,30 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
}, openPeer: { _, _, _ in
|
||||
}, openPeerMention: { _ in
|
||||
}, openMessageContextMenu: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in
|
||||
}, clickThroughMessage: {
|
||||
}, toggleMessagesSelection: { _, _ in
|
||||
}, sendMessage: { _ in
|
||||
}, sendSticker: { _, _ in
|
||||
}, sendGif: { _ in
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
}, openUrl: { _, _, _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
}, openInstantPage: { _, _ in
|
||||
}, openWallpaper: { _ in
|
||||
}, openHashtag: { _, _ in
|
||||
}, updateInputState: { _ in
|
||||
}, updateInputMode: { _ in
|
||||
}, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in
|
||||
}, callPeer: { _ in
|
||||
@ -76,8 +98,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
|
||||
}, seekToTimecode: { _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState())
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState())
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
@ -197,14 +197,7 @@ final public class PasscodeEntryController: ViewController {
|
||||
|
||||
self.controllerNode.activateInput()
|
||||
if self.arguments.animated {
|
||||
let iconFrame = self.arguments.lockIconInitialFrame()
|
||||
if !iconFrame.isEmpty {
|
||||
Queue.mainQueue().after(0.5) {
|
||||
serviceSoundManager.playLockSound()
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.animateIn(iconFrame: iconFrame, completion: { [weak self] in
|
||||
self.controllerNode.animateIn(iconFrame: self.arguments.lockIconInitialFrame(), completion: { [weak self] in
|
||||
self?.presentationCompleted?()
|
||||
})
|
||||
} else {
|
||||
|
||||
@ -182,6 +182,7 @@ public class PeerMediaCollectionController: TelegramController {
|
||||
},sendSticker: { _, _ in
|
||||
}, sendGif: { _ in
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
}, openUrl: { [weak self] url, _, external in
|
||||
self?.openUrl(url, external: external ?? false)
|
||||
|
||||
@ -647,5 +647,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
|
||||
controller.present(c, in: .window(.root), with: p)
|
||||
}
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
@ -97,7 +97,6 @@ private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile,
|
||||
|
||||
private func chatMessageStickerPackThumbnailData(postbox: Postbox, representation: TelegramMediaImageRepresentation, synchronousLoad: Bool) -> Signal<Data?, NoError> {
|
||||
let resource = representation.resource
|
||||
|
||||
let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: CGSize(width: 160.0, height: 160.0)), complete: false, fetch: false, attemptSynchronously: synchronousLoad)
|
||||
|
||||
return maybeFetched
|
||||
@ -131,7 +130,21 @@ private func chatMessageStickerPackThumbnailData(postbox: Postbox, representatio
|
||||
}
|
||||
}
|
||||
|
||||
func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> {
|
||||
func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<MediaResourceData, NoError> {
|
||||
let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedAnimatedStickerRepresentation(), pathExtension: "mp4", complete: false, fetch: false, attemptSynchronously: synchronousLoad)
|
||||
|
||||
return maybeFetched
|
||||
|> take(1)
|
||||
|> mapToSignal { maybeData in
|
||||
if maybeData.complete {
|
||||
return .single(maybeData)
|
||||
} else {
|
||||
return postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedAnimatedStickerRepresentation(), pathExtension: "mp4", complete: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func chatMessageAnimatedStrickerBackingData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> {
|
||||
let resource = fileReference.media.resource
|
||||
|
||||
let maybeFetched = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)
|
||||
@ -144,8 +157,8 @@ func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReferenc
|
||||
return .single((loadedData, true))
|
||||
} else {
|
||||
let fullSizeData = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)
|
||||
|> map { next -> (Data?, Bool) in
|
||||
return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)
|
||||
|> map { next -> (Data?, Bool) in
|
||||
return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)
|
||||
}
|
||||
return fullSizeData
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ module TelegramUIPrivateModule {
|
||||
header "../EDSunriseSet.h"
|
||||
header "../TGBridgeAudioDecoder.h"
|
||||
header "../TGBridgeAudioEncoder.h"
|
||||
header "../GZip.h"
|
||||
private header "../../third-party/libjpeg-turbo/turbojpeg.h"
|
||||
private header "../../third-party/libjpeg-turbo/jpeglib.h"
|
||||
}
|
||||
|
||||