diff --git a/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json new file mode 100644 index 0000000000..c1829fc87f --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png new file mode 100644 index 0000000000..1437fe9486 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@3x.png b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@3x.png new file mode 100644 index 0000000000..dd30c738b6 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/Contents.json new file mode 100644 index 0000000000..829a7f053b --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@2x.png b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@2x.png new file mode 100644 index 0000000000..9214104f0a Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@3x.png b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@3x.png new file mode 100644 index 0000000000..38f3b0d75c Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatLink.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/Contents.json new file mode 100644 index 0000000000..71aa1b3552 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@2x.png b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@2x.png new file mode 100644 index 0000000000..88196fee34 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@3x.png b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@3x.png new file mode 100644 index 0000000000..a76a25e10a Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json new file mode 100644 index 0000000000..9af7e0f338 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png new file mode 100644 index 0000000000..d74c0d35bc Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@3x.png b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@3x.png new file mode 100644 index 0000000000..7c51e9d406 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json new file mode 100644 index 0000000000..6ebf2a685d --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png new file mode 100644 index 0000000000..1b30548a1f Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@3x.png b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@3x.png new file mode 100644 index 0000000000..5263039156 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 47106d50ba..8aad2898c9 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -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 = ""; }; 091417F121EF4E5D00C8325A /* WallpaperGalleryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperGalleryController.swift; sourceTree = ""; }; 091417F321EF4F5F00C8325A /* WallpaperGalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperGalleryItem.swift; sourceTree = ""; }; + 09167E1B22973A14005734A7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 091954722294591B00E11046 /* AnimatedStickerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerNode.swift; sourceTree = ""; }; + 09195474229474E900E11046 /* AnimatedStickerPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerPlayerManager.swift; sourceTree = ""; }; + 091954762294752C00E11046 /* AnimatedStickerPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerPlayer.swift; sourceTree = ""; }; + 091954782294754E00E11046 /* AnimatedStickerUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerUtils.swift; sourceTree = ""; }; + 0919547A2294788200E11046 /* AnimatedStickerVideoCompositor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerVideoCompositor.swift; sourceTree = ""; }; + 0921F5FB228B01B6001A13D7 /* GZip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GZip.h; sourceTree = ""; }; + 0921F5FC228B01B6001A13D7 /* GZip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GZip.m; sourceTree = ""; }; + 0921F60A228C8765001A13D7 /* ItemListPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListPlaceholderItem.swift; sourceTree = ""; }; + 0921F60D228EE000001A13D7 /* ChatMessageActionUrlAuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageActionUrlAuthController.swift; sourceTree = ""; }; 092F368C2154AAE9001A9F49 /* SFCompactRounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SFCompactRounded-Semibold.otf"; sourceTree = ""; }; 092F368F2157AB46001A9F49 /* ItemListCallListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListCallListItem.swift; sourceTree = ""; }; 09310D1A213BC5DE0020033A /* anim_ungroup.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_ungroup.json; sourceTree = ""; }; @@ -2532,6 +2550,19 @@ name = "Language Suggestion"; sourceTree = ""; }; + 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 = ""; + }; 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 = ""; @@ -4894,6 +4927,7 @@ D0B2F76D2052B59F00D3BFB9 /* InviteContactsController.swift */, D0B2F76F2052B5A800D3BFB9 /* InviteContactsControllerNode.swift */, D0B2F7712052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift */, + 0921F60A228C8765001A13D7 /* ItemListPlaceholderItem.swift */, ); name = Contacts; sourceTree = ""; @@ -4981,6 +5015,8 @@ 09E4A800223AE1B30038140F /* PeerType.swift */, 09E4A806223D4B860038140F /* AccountUtils.swift */, D099E21F229405BB00561B75 /* Weak.swift */, + 0921F5FB228B01B6001A13D7 /* GZip.h */, + 0921F5FC228B01B6001A13D7 /* GZip.m */, ); name = Utils; sourceTree = ""; @@ -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 */, diff --git a/TelegramUI/AnimatedStickerNode.swift b/TelegramUI/AnimatedStickerNode.swift new file mode 100644 index 0000000000..18e0a07faf --- /dev/null +++ b/TelegramUI/AnimatedStickerNode.swift @@ -0,0 +1,12 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AVFoundation +import CoreImage + +final class AnimatedStickerNode: ASDisplayNode { + +} diff --git a/TelegramUI/AnimatedStickerPlayer.swift b/TelegramUI/AnimatedStickerPlayer.swift new file mode 100644 index 0000000000..742ef93d8b --- /dev/null +++ b/TelegramUI/AnimatedStickerPlayer.swift @@ -0,0 +1,5 @@ +import UIKit + +class AnimatedStickerPlayer: NSObject { + +} diff --git a/TelegramUI/AnimatedStickerPlayerManager.swift b/TelegramUI/AnimatedStickerPlayerManager.swift new file mode 100644 index 0000000000..b1cf03b6ea --- /dev/null +++ b/TelegramUI/AnimatedStickerPlayerManager.swift @@ -0,0 +1,5 @@ +import Foundation + +final class AnimatedStickerPlayerManager { + +} diff --git a/TelegramUI/AnimatedStickerUtils.swift b/TelegramUI/AnimatedStickerUtils.swift new file mode 100644 index 0000000000..fc7a2931fa --- /dev/null +++ b/TelegramUI/AnimatedStickerUtils.swift @@ -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 { + 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.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)) +} diff --git a/TelegramUI/AnimatedStickerVideoCompositor.swift b/TelegramUI/AnimatedStickerVideoCompositor.swift new file mode 100644 index 0000000000..cbadf2f9b2 --- /dev/null +++ b/TelegramUI/AnimatedStickerVideoCompositor.swift @@ -0,0 +1,5 @@ +import AVFoundation + +final class AnimatedStickerVideoCompositor: NSObject { + +} diff --git a/TelegramUI/CachedResourceRepresentations.swift b/TelegramUI/CachedResourceRepresentations.swift index 1d077504d1..ebbc35d3fb 100644 --- a/TelegramUI/CachedResourceRepresentations.swift +++ b/TelegramUI/CachedResourceRepresentations.swift @@ -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 + } + } +} diff --git a/TelegramUI/ChatAnimationGalleryItem.swift b/TelegramUI/ChatAnimationGalleryItem.swift index b4412e6df7..3366b7a3ec 100644 --- a/TelegramUI/ChatAnimationGalleryItem.swift +++ b/TelegramUI/ChatAnimationGalleryItem.swift @@ -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 in if completed, let data = data { return .single(data) diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 1bb44e3a02..9c5d42db0f 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -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) + } } } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index e2195fab48..69ed212120 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -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() diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index e16d330aa3..9b3f684635 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -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 diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index fde439917e..6c1825609c 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -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 diff --git a/TelegramUI/ChatMessageActionUrlAuthController.swift b/TelegramUI/ChatMessageActionUrlAuthController.swift new file mode 100644 index 0000000000..de310b6117 --- /dev/null +++ b/TelegramUI/ChatMessageActionUrlAuthController.swift @@ -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 +} diff --git a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 3241be1f09..73250443f7 100644 --- a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -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) { - 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 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 } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index f0ed6cf146..35b4bb6ae0 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -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 } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index fecfb4326b..3b923e628f 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -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) } } } diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 4462517b81..e52ab20628 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -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) } diff --git a/TelegramUI/ChatTextInputAttributes.swift b/TelegramUI/ChatTextInputAttributes.swift index a454389769..3e1ac3ca05 100644 --- a/TelegramUI/ChatTextInputAttributes.swift +++ b/TelegramUI/ChatTextInputAttributes.swift @@ -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 { diff --git a/TelegramUI/ContactListActionItem.swift b/TelegramUI/ContactListActionItem.swift index 44b624a02f..048a25d21f 100644 --- a/TelegramUI/ContactListActionItem.swift +++ b/TelegramUI/ContactListActionItem.swift @@ -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)) } }) } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index f0b10d8bff..ee22c9f949 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -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)) diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 4f292bd0b7..6e23be2b0f 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -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) diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index 67c5a5b3f4..6869b97dc1 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -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 } diff --git a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift index cfb1e5b1ad..ea0beed23b 100644 --- a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift @@ -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) diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index ba13566825..3d36c8c149 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -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 { 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 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 { + 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()) +} diff --git a/TelegramUI/GZip.h b/TelegramUI/GZip.h new file mode 100644 index 0000000000..144366f3e2 --- /dev/null +++ b/TelegramUI/GZip.h @@ -0,0 +1,17 @@ +#ifndef Telegram_GZip_h +#define Telegram_GZip_h + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +NSData *TGGZipData(NSData *data, float level); +NSData *TGGUnzipData(NSData *data); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/TelegramUI/GZip.m b/TelegramUI/GZip.m new file mode 100644 index 0000000000..811c408e5c --- /dev/null +++ b/TelegramUI/GZip.m @@ -0,0 +1,79 @@ +#import "GZip.h" + +#import + +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; +} diff --git a/TelegramUI/ItemListPlaceholderItem.swift b/TelegramUI/ItemListPlaceholderItem.swift new file mode 100644 index 0000000000..8b1d6c9a79 --- /dev/null +++ b/TelegramUI/ItemListPlaceholderItem.swift @@ -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?, (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) + } +} diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 7df6ecea6d..e6c36fbdde 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -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 { diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 97d4025a53..424c431fcd 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -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) diff --git a/TelegramUI/PasscodeEntryController.swift b/TelegramUI/PasscodeEntryController.swift index 148c87be91..837ba5cb2e 100644 --- a/TelegramUI/PasscodeEntryController.swift +++ b/TelegramUI/PasscodeEntryController.swift @@ -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 { diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 502f078f98..88bdda722f 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -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) diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index 784e5a1c7a..f8e3da7319 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -647,5 +647,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont controller.present(c, in: .window(.root), with: p) } } + return controller } diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index ae285b70d2..b0fa1bde4d 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -97,7 +97,6 @@ private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile, private func chatMessageStickerPackThumbnailData(postbox: Postbox, representation: TelegramMediaImageRepresentation, synchronousLoad: Bool) -> Signal { 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 { + 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 } diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 1c26f10ede..84f9825481 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -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" }