diff --git a/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/Contents.json new file mode 100644 index 0000000000..0b72936b64 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_bot.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/ic_bot.pdf b/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/ic_bot.pdf new file mode 100644 index 0000000000..9d7a87f262 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/ic_bot.pdf differ diff --git a/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/Contents.json new file mode 100644 index 0000000000..5785acf156 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_keyboard.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/ic_keyboard.pdf b/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/ic_keyboard.pdf new file mode 100644 index 0000000000..b713289f1b Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/ic_keyboard.pdf differ diff --git a/Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/Contents.json new file mode 100644 index 0000000000..ef827e8385 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_sticker.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/IconAttachment.pdf b/Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/ic_sticker.pdf similarity index 54% rename from Images.xcassets/Chat/Input/Text/IconAttachment.imageset/IconAttachment.pdf rename to Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/ic_sticker.pdf index 0a160807a6..ee4036d5e9 100644 Binary files a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/IconAttachment.pdf and b/Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/ic_sticker.pdf differ diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json index ae4dac8782..a4e9ce4cfd 100644 --- a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "IconAttachment.pdf" + "filename" : "ic_attach.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf new file mode 100644 index 0000000000..3a75f7a4f7 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf differ diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json new file mode 100644 index 0000000000..72555bf44c --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_voice.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf new file mode 100644 index 0000000000..b5eb5080f5 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json new file mode 100644 index 0000000000..ecfd165d98 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Send.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf new file mode 100644 index 0000000000..5ba0bfdaf1 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf differ diff --git a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Contents.json b/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Contents.json index 6d499c7d57..96a7e393b7 100644 --- a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Contents.json +++ b/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Contents.json @@ -6,7 +6,7 @@ }, { "idiom" : "universal", - "filename" : "builtin-wallpaper-0.jpg", + "filename" : "Dogs BG.jpg", "scale" : "2x" }, { diff --git a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg b/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg new file mode 100644 index 0000000000..310fe0dbe6 Binary files /dev/null and b/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg differ diff --git a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/builtin-wallpaper-0.jpg b/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/builtin-wallpaper-0.jpg deleted file mode 100644 index ea45b36ee0..0000000000 Binary files a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/builtin-wallpaper-0.jpg and /dev/null differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 9701f901f6..3cc96206d7 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ D00370301DA43077004308D3 /* PeerInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003702F1DA43077004308D3 /* PeerInfoItem.swift */; }; D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */; }; D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; }; + D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */; }; + D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */; }; + D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */; }; + D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */; }; D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; }; D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; }; D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0761D9190EF000889C2 /* GridMessageItem.swift */; }; @@ -19,8 +23,13 @@ D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; }; D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; }; D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; }; + D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; + D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */; }; D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */; }; + D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */; }; + D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */; }; + D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */; }; D08D452E1D5E340300A7428A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D45291D5E340300A7428A /* AsyncDisplayKit.framework */; }; D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; }; D08D45301D5E340300A7428A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452B1D5E340300A7428A /* Postbox.framework */; }; @@ -93,7 +102,6 @@ D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */; }; D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */; }; D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */; }; - D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D901D6B87EC0046BCD6 /* Cache.swift */; }; D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; }; D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */; }; D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */; }; @@ -222,6 +230,10 @@ D003702F1DA43077004308D3 /* PeerInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoItem.swift; sourceTree = ""; }; D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoTextWithLabelItem.swift; sourceTree = ""; }; D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; + D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputNode.swift; sourceTree = ""; }; + D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputNode.swift; sourceTree = ""; }; + D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputNodes.swift; sourceTree = ""; }; + D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerPackItem.swift; sourceTree = ""; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; @@ -230,8 +242,13 @@ D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = ""; }; D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = ""; }; D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; + D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; + D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = ""; }; D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = ""; }; + D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = ""; }; + D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = ""; }; + D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = ""; }; D08D45291D5E340300A7428A /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AsyncDisplayKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/AsyncDisplayKit.framework"; sourceTree = ""; }; D08D452A1D5E340300A7428A /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Display.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Display.framework"; sourceTree = ""; }; D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; @@ -307,7 +324,6 @@ D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlaybackData.swift; sourceTree = ""; }; D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaVideoFrameDecoder.swift; sourceTree = ""; }; D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackFrameDecoder.swift; sourceTree = ""; }; - D0F69D901D6B87EC0046BCD6 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = TelegramUI.xcconfig; path = TelegramUI/Config/TelegramUI.xcconfig; sourceTree = ""; }; D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionHeaderNode.swift; sourceTree = ""; }; D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextNode.swift; sourceTree = ""; }; @@ -470,6 +486,27 @@ name = Components; sourceTree = ""; }; + D021E0CC1DB4132E00C6B04F /* Input Nodes */ = { + isa = PBXGroup; + children = ( + D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */, + D021E0E31DB55CDB00C6B04F /* Media */, + ); + name = "Input Nodes"; + sourceTree = ""; + }; + D021E0E31DB55CDB00C6B04F /* Media */ = { + isa = PBXGroup; + children = ( + D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */, + D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */, + D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */, + D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */, + D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */, + ); + name = Media; + sourceTree = ""; + }; D02BE0751D9190CD000889C2 /* Grid Items */ = { isa = PBXGroup; children = ( @@ -492,6 +529,7 @@ D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */, D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */, D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */, + D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */, ); name = "Interface State"; sourceTree = ""; @@ -686,7 +724,6 @@ children = ( D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */, - D0F69D901D6B87EC0046BCD6 /* Cache.swift */, D0F69DBC1D6B886C0046BCD6 /* Player */, D0F69E9D1D6B8E240046BCD6 /* Resources */, ); @@ -867,9 +904,10 @@ D0F69E181D6B8AD10046BCD6 /* Items */, D03ADB461D703250005A521C /* Interface State */, D03ADB491D704427005A521C /* Accessory Panels */, + D021E0CC1DB4132E00C6B04F /* Input Nodes */, D0DF0C961D81FD87008AEB01 /* Input Context Panels */, - D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */, D0BA6F811D784C3A0034826E /* Input Panels */, + D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */, D0F69E441D6B8B850046BCD6 /* History Navigation */, D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */, ); @@ -1033,6 +1071,8 @@ D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */, D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */, D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */, + D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */, + D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */, ); name = Resources; sourceTree = ""; @@ -1205,10 +1245,13 @@ D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */, D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */, + D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */, D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */, + D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */, D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, + D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */, @@ -1229,8 +1272,10 @@ D0DE77291D932923002B8809 /* GridMessageSelectionNode.swift in Sources */, D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */, D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */, + D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */, D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */, + D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */, D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */, D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */, D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */, @@ -1249,6 +1294,7 @@ D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */, D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, + D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */, D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */, D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, @@ -1272,13 +1318,16 @@ D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */, D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */, D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */, + D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */, + D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */, D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */, D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */, D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */, D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */, D0DE77321D940295002B8809 /* ListMessageFileItemNode.swift in Sources */, + D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */, D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */, D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */, D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */, @@ -1296,7 +1345,6 @@ D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */, D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */, D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, - D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */, D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */, D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, diff --git a/TelegramUI/ActionSheetRollImageItem.swift b/TelegramUI/ActionSheetRollImageItem.swift index 5c2da247a4..73a1d79b51 100644 --- a/TelegramUI/ActionSheetRollImageItem.swift +++ b/TelegramUI/ActionSheetRollImageItem.swift @@ -7,8 +7,14 @@ private let testBackground = generateStretchableFilledCircleImage(radius: 8.0, c final class ActionSheetRollImageItem: ListViewItem { let asset: PHAsset + let selectedItem: () -> Void - init(asset: PHAsset) { + var selectable: Bool { + return true + } + + init(asset: PHAsset, selected: @escaping () -> Void) { + self.selectedItem = selected self.asset = asset } @@ -27,6 +33,10 @@ final class ActionSheetRollImageItem: ListViewItem { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { }) } + + func selected(listView: ListView) { + self.selectedItem() + } } private final class ActionSheetRollImageItemNode: ListViewItemNode { diff --git a/TelegramUI/Cache.swift b/TelegramUI/Cache.swift deleted file mode 100644 index 75ec105480..0000000000 --- a/TelegramUI/Cache.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import SwiftSignalKit -import Display -import TelegramCore - -let threadPool = ThreadPool(threadCount: 4, threadPriority: 0.2) - -func cachedCloudFileLocation(_ location: TelegramCloudMediaLocation) -> Signal { - return Signal { subscriber in - assertNotOnMainThread() - switch location.apiInputLocation { - case let .inputFileLocation(volumeId, localId, _): - let path = NSTemporaryDirectory() + "/\(location.datacenterId)_\(volumeId)_\(localId)" - do { - let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) - subscriber.putNext(data) - subscriber.putCompletion() - } catch { - subscriber.putError(NoError()) - } - - case _: - subscriber.putError(NoError()) - } - return ActionDisposable { - - } - } -} - -func cacheCloudFileLocation(_ location: TelegramCloudMediaLocation, data: Data) { - assertNotOnMainThread() - switch location.apiInputLocation { - case let .inputFileLocation(volumeId, localId, _): - let path = NSTemporaryDirectory() + "/\(location.datacenterId)_\(volumeId)_\(localId)" - let _ = try? data.write(to: URL(fileURLWithPath: path), options: [.atomicWrite]) - case _: - break - } -} diff --git a/TelegramUI/CachedResourceRepresentations.swift b/TelegramUI/CachedResourceRepresentations.swift new file mode 100644 index 0000000000..996e6ab7b6 --- /dev/null +++ b/TelegramUI/CachedResourceRepresentations.swift @@ -0,0 +1,27 @@ +import Foundation +import Postbox +import SwiftSignalKit + +final class CachedStickerAJpegRepresentation: CachedMediaResourceRepresentation { + let size: CGSize? + + var uniqueId: String { + if let size = self.size { + return "sticker-ajpeg-\(Int(size.width))x\(Int(size.height))" + } else { + return "sticker-ajpeg" + } + } + + init(size: CGSize?) { + self.size = size + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedStickerAJpegRepresentation { + return self.size == to.size + } else { + return false + } + } +} diff --git a/TelegramUI/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift index c2679d9daf..d547a886c1 100644 --- a/TelegramUI/ChannelInfoEntries.swift +++ b/TelegramUI/ChannelInfoEntries.swift @@ -153,7 +153,7 @@ enum ChannelInfoEntry: PeerInfoEntry { func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { switch self { case let .info(peer, cachedData): - return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain) + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, editingState: nil, sectionId: self.section.rawValue, style: .plain) case let .about(text): return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue) case let .userName(value): @@ -173,7 +173,7 @@ enum ChannelInfoEntry: PeerInfoEntry { label = "Enabled" } return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: { - interaction.changeNotificationNoteSettings() + interaction.changeNotificationMuteSettings() }) case .report: return PeerInfoActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { @@ -187,7 +187,7 @@ enum ChannelInfoEntry: PeerInfoEntry { } } -func channelBroadcastInfoEntries(view: PeerView) -> [PeerInfoEntry] { +func channelBroadcastInfoEntries(view: PeerView) -> PeerInfoEntries { var entries: [PeerInfoEntry] = [] entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) if let cachedChannelData = view.cachedData as? CachedChannelData { @@ -206,5 +206,5 @@ func channelBroadcastInfoEntries(view: PeerView) -> [PeerInfoEntry] { entries.append(ChannelInfoEntry.leave) } } - return entries + return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil) } diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 843c1e3c3b..76adf0c4fd 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -49,23 +49,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let actionDisposable = MetaDisposable() - override var peer: Peer? { - didSet { - if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) { - if let action = actionForPeer(peer) { - self.action = action - let (title, color) = titleAndColorForAction(action) - self.button.setTitle(title, for: []) - self.button.setTitleColor(color, for: [.normal]) - self.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) - self.button.sizeToFit() - self.setNeedsLayout() - } else { - self.action = nil - } - } - } - } + private var presentationInterfaceState = ChatPresentationInterfaceState() override init() { self.button = UIButton() @@ -84,23 +68,6 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.actionDisposable.dispose() } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 45.0) - } - - override func layout() { - super.layout() - - let bounds = self.bounds - - let buttonSize = self.button.bounds.size - self.button.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - buttonSize.width) / 2.0), y: floor((bounds.size.height - buttonSize.height) / 2.0)), size: buttonSize) - - //_activityIndicator.frame = CGRectMake(self.frame.size.width - _activityIndicator.frame.size.width - 12.0f, CGFloor((self.frame.size.height - _activityIndicator.frame.size.height) / 2.0f), _activityIndicator.frame.size.width, _activityIndicator.frame.size.height); - let indicatorSize = self.activityIndicator.bounds.size - self.activityIndicator.frame = CGRect(origin: CGPoint(x: bounds.size.width - indicatorSize.width - 12.0, y: floor((bounds.size.height - indicatorSize.height) / 2.0)), size: indicatorSize) - } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { return self.button @@ -110,7 +77,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } @objc func buttonPressed() { - guard let account = self.account, let action = self.action, let peer = self.peer else { + guard let account = self.account, let action = self.action, let peer = self.presentationInterfaceState.peer else { return } @@ -135,4 +102,34 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { break } } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + let previousState = self.presentationInterfaceState + self.presentationInterfaceState = interfaceState + + if let peer = interfaceState.peer, previousState.peer == nil || !peer.isEqual(previousState.peer!) { + if let action = actionForPeer(peer) { + self.action = action + let (title, color) = titleAndColorForAction(action) + self.button.setTitle(title, for: []) + self.button.setTitleColor(color, for: [.normal]) + self.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) + self.button.sizeToFit() + } else { + self.action = nil + } + } + } + + let panelHeight: CGFloat = 47.0 + + let buttonSize = self.button.bounds.size + self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + let indicatorSize = self.activityIndicator.bounds.size + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + + return 47.0 + } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index f14b96163b..7d894519a9 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -22,7 +22,7 @@ public class ChatController: ViewController { private var didSetPeerReady = false private let peerView = Promise() - private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil, inputContext: nil) + private var presentationInterfaceState = ChatPresentationInterfaceState() private let chatInterfaceStatePromise = Promise() private var chatTitleView: ChatTitleView? @@ -154,13 +154,17 @@ public class ChatController: ViewController { } } }, clickThroughMessage: { [weak self] in - self?.view.endEditing(true) + self?.chatDisplayNode.dismissInput() }, toggleMessageSelection: { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - strongSelf.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } }) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } }) } } + }, sendSticker: { [weak self] file in + if let strongSelf = self { + enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: file).start() + } }) self.controllerInteraction = controllerInteraction @@ -173,7 +177,7 @@ public class ChatController: ViewController { chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) - self.updateChatPresentationInterfaceState(animated: false, { return $0 }) + self.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0 }) self.peerView.set(account.viewTracker.peerView(peerId)) @@ -184,7 +188,7 @@ public class ChatController: ViewController { strongSelf.chatTitleView?.peerView = peerView (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) } - strongSelf.updateChatPresentationInterfaceState(animated: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] } }) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] } }) if !strongSelf.didSetPeerReady { strongSelf.didSetPeerReady = true strongSelf._peerReady.set(.single(true)) @@ -288,12 +292,23 @@ public class ChatController: ViewController { } self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in - self?.updateChatPresentationInterfaceState(animated: animated, { $0.updatedInterfaceState(f) }) + self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, { $0.updatedInterfaceState(f) }) } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in if let strongSelf = self { let controller = ChatMediaActionSheetController() + controller.photo = { [weak strongSelf] asset in + if let strongSelf = strongSelf { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight)) + let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) + let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: media).start() + } + } controller.location = { [weak strongSelf] in if let strongSelf = strongSelf { let mapInputController = MapInputController() @@ -317,14 +332,14 @@ public class ChatController: ViewController { let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(message.id) } }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(message.id) } }) strongSelf.chatDisplayNode.ensureInputViewFocused() } } }, beginMessageSelection: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } }) } } }, deleteSelectedMessages: { [weak self] in @@ -334,7 +349,7 @@ public class ChatController: ViewController { modifier.deleteMessages(Array(messageIds)) }).start() } - strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { @@ -343,7 +358,11 @@ public class ChatController: ViewController { } }, updateTextInputState: { [weak self] textInputState in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } }) + } + }, updateInputMode: { [weak self] f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputMode(f) }) } }) @@ -362,6 +381,8 @@ public class ChatController: ViewController { self.chatDisplayNode.historyNode.preloadPages = true self.chatDisplayNode.historyNode.canReadHistory.set(.single(true)) + + self.chatDisplayNode.loadInputPanels() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -374,13 +395,14 @@ public class ChatController: ViewController { }) } - func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { + func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) let inputContext = inputContextForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account) - let updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputContext { _ in return inputContext } + let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account) + let updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputContext({ _ in return inputContext }).updatedInputTextPanelState({ _ in return inputTextPanelState }) if self.isNodeLoaded { - self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated) + self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive) } self.presentationInterfaceState = updatedChatPresentationInterfaceState self.chatInterfaceStatePromise.set(.single(updatedChatPresentationInterfaceState.interfaceState)) @@ -429,7 +451,7 @@ public class ChatController: ViewController { private func navigationButtonAction(_ action: ChatNavigationButtonAction) { switch action { case .cancelMessageSelection: - self.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) case .clearHistory: let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index e726f11eaa..3f82264aaa 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import AsyncDisplayKit +import TelegramCore public enum ChatControllerInteractionNavigateToPeer { case chat @@ -16,13 +17,15 @@ public final class ChatControllerInteraction { var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? let toggleMessageSelection: (MessageId) -> Void + let sendSticker: (TelegramMediaFile) -> Void - public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void) { self.openMessage = openMessage self.openPeer = openPeer self.openMessageContextMenu = openMessageContextMenu self.navigateToMessage = navigateToMessage self.clickThroughMessage = clickThroughMessage self.toggleMessageSelection = toggleMessageSelection + self.sendSticker = sendSticker } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index e12f3a711e..c223f7eab3 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -15,6 +15,7 @@ private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: Ch class ChatControllerNode: ASDisplayNode { let account: Account let peerId: PeerId + let controllerInteraction: ChatControllerInteraction let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode @@ -26,7 +27,10 @@ class ChatControllerNode: ASDisplayNode { private var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? + private var inputNode: ChatInputNode? + private var textInputPanelNode: ChatTextInputPanelNode? + private var inputMediaNode: ChatMediaInputNode? let navigateToLatestButton: ChatHistoryNavigationButtonNode @@ -41,9 +45,15 @@ class ChatControllerNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? + private var containerLayoutAndNavigationBarHeight: (ContainerViewLayout, CGFloat)? + + private var scheduledLayoutTransitionRequestId: Int = 0 + private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) { self.account = account self.peerId = peerId + self.controllerInteraction = controllerInteraction self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -54,11 +64,11 @@ class ChatControllerNode: ASDisplayNode { self.historyNode = ChatHistoryListNode(account: account, peerId: peerId, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction) self.inputPanelBackgroundNode = ASDisplayNode() - self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa) + self.inputPanelBackgroundNode.backgroundColor = UIColor(0xF5F6F8) self.inputPanelBackgroundNode.isLayerBacked = true self.inputPanelBackgroundSeparatorNode = ASDisplayNode() - self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xcdccd3) + self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xC9CDD1) self.inputPanelBackgroundSeparatorNode.isLayerBacked = true self.navigateToLatestButton = ChatHistoryNavigationButtonNode() @@ -113,14 +123,58 @@ class ChatControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) { - var insets = layout.insets(options: [.input]) + //print("update \(self.scheduledLayoutTransitionRequest)") + self.scheduledLayoutTransitionRequest = nil + var previousInputHeight: CGFloat = 0.0 + if let (previousLayout, _) = self.containerLayoutAndNavigationBarHeight { + previousInputHeight = previousLayout.insets(options: [.input]).bottom + } + if let inputNode = self.inputNode { + previousInputHeight = inputNode.bounds.size.height + } + self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) + + var dismissedInputNode: ChatInputNode? + var immediatelyLayoutInputNodeAndAnimateAppearance = false + var inputNodeHeight: CGFloat? + if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction) { + if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + inputTextPanelNode.ensureUnfocused() + } + if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil { + self.inputMediaNode = inputMediaNode + } + if self.inputNode != inputNode { + dismissedInputNode = self.inputNode + self.inputNode = inputNode + immediatelyLayoutInputNodeAndAnimateAppearance = true + self.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode) + } + inputNodeHeight = inputNode.updateLayout(width: layout.size.width, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + } else if let inputNode = self.inputNode { + dismissedInputNode = inputNode + self.inputNode = nil + } + + if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { + inputMediaNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + } + + var insets: UIEdgeInsets + if let inputNodeHeight = inputNodeHeight { + insets = layout.insets(options: []) + insets.bottom += inputNodeHeight + } else { + insets = layout.insets(options: [.input]) + } insets.top += navigationBarHeight var duration: Double = 0.0 var curve: UInt = 0 + var animated = true switch transition { case .immediate: - break + animated = false case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { @@ -150,16 +204,19 @@ class ChatControllerNode: ASDisplayNode { var inputPanelSize: CGSize? var immediatelyLayoutInputPanelAndAnimateAppearance = false if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { - inputPanelSize = inputPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) - if inputPanelNode !== self.inputPanelNode { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() } dismissedInputPanelNode = self.inputPanelNode immediatelyLayoutInputPanelAndAnimateAppearance = true + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode self.insertSubnode(inputPanelNode, belowSubnode: self.navigateToLatestButton) + } else { + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, transition: transition, interfaceState: self.chatPresentationInterfaceState) + inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { dismissedInputPanelNode = self.inputPanelNode @@ -234,7 +291,7 @@ class ChatControllerNode: ASDisplayNode { let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) - transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: inputBackgroundFrame.origin, size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame) if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) { @@ -245,7 +302,6 @@ class ChatControllerNode: ASDisplayNode { transition.updateFrame(node: inputPanelNode, frame: inputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) - inputPanelNode.updateFrames(transition: transition) } if let accessoryPanelNode = self.accessoryPanelNode, let accessoryPanelFrame = accessoryPanelFrame, !accessoryPanelNode.frame.equalTo(accessoryPanelFrame) { @@ -271,6 +327,19 @@ class ChatControllerNode: ASDisplayNode { } } + if let inputNode = self.inputNode, let inputNodeHeight = inputNodeHeight { + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) + if immediatelyLayoutInputNodeAndAnimateAppearance { + var adjustedForPreviousInputHeightFrame = inputNodeFrame + let heightDifference = inputNodeHeight - previousInputHeight + adjustedForPreviousInputHeightFrame.origin.y += heightDifference + inputNode.frame = adjustedForPreviousInputHeightFrame + transition.updateFrame(node: inputNode, frame: inputNodeFrame) + } else { + transition.updateFrame(node: inputNode, frame: inputNodeFrame) + } + } + if let dismissedInputPanelNode = dismissedInputPanelNode { var frameCompleted = false var alphaCompleted = false @@ -339,14 +408,34 @@ class ChatControllerNode: ASDisplayNode { completed() }) } + + if let dismissedInputNode = dismissedInputNode { + transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - CGFloat(FLT_EPSILON)), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), completion: { [weak dismissedInputNode] _ in + dismissedInputNode?.removeFromSupernode() + }) + } } - func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool) { + private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { + switch state.inputMode { + case .text: + if state.interfaceState.selectionState != nil { + return false + } else { + return true + } + default: + return false + } + } + + func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool, interactive: Bool) { if let textInputPanelNode = self.textInputPanelNode { self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) } } if self.chatPresentationInterfaceState != chatPresentationInterfaceState { + var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState self.chatPresentationInterfaceState = chatPresentationInterfaceState @@ -354,8 +443,28 @@ class ChatControllerNode: ASDisplayNode { textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState } - if !self.ignoreUpdateHeight { - self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + let layoutTransition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + + if updatedInputFocus { + if !self.ignoreUpdateHeight { + self.scheduleLayoutTransitionRequest(layoutTransition) + } + + if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) { + self.ensureInputViewFocused() + } else { + if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + inputTextPanelNode.ensureUnfocused() + } + } + } else { + if !self.ignoreUpdateHeight { + if interactive { + self.scheduleLayoutTransitionRequest(layoutTransition) + } else { + self.requestLayout(layoutTransition) + } + } } } } @@ -368,7 +477,39 @@ class ChatControllerNode: ASDisplayNode { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if recognizer.state == .ended { - self.view.endEditing(true) + self.dismissInput() } } + + func dismissInput() { + switch self.chatPresentationInterfaceState.inputMode { + case .none: + break + default: + self.interfaceInteraction?.updateInputMode({ _ in .none }) + } + } + + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { + let requestId = self.scheduledLayoutTransitionRequestId + self.scheduledLayoutTransitionRequestId += 1 + self.scheduledLayoutTransitionRequest = (requestId, transition) + (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in + if let strongSelf = self { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest { + strongSelf.scheduledLayoutTransitionRequest = nil + strongSelf.requestLayout(currentRequestTransition) + } + } + }) + self.setNeedsLayout() + } + + func loadInputPanels() { + /*if self.inputMediaNode == nil { + let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction) + inputNode.interfaceInteraction = interfaceInteraction + self.inputMediaNode = inputNode + }*/ + } } diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index a274cef0c9..d4562ec0f5 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -48,7 +48,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { private var accountAndFile: (Account, TelegramMediaFile)? private let dataDisposable = MetaDisposable() - private var isVisible = false + private var itemIsVisible = false override init() { if #available(iOS 9.0, *) { @@ -93,7 +93,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { if let fileName = file.fileName { pathExtension = (fileName as NSString).pathExtension } - let data = account.postbox.mediaBox.resourceData(CloudFileMediaResource(location: file.location, size: file.size), pathExtension: pathExtension, complete: true) + let data = account.postbox.mediaBox.resourceData(file.resource, pathExtension: pathExtension, complete: true) |> deliverOnMainQueue self.dataDisposable.set(data.start(next: { [weak self] data in if let strongSelf = self { diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index b7ee37e69d..aaeb695b10 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -284,18 +284,18 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { var updateLayout: GridNodeUpdateLayout? if let updateSizeAndInsets = updateSizeAndInsets { - updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, itemSize: CGSize(width: 200.0, height: 200.0), indexOffset: 0), transition: .immediate) + updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, itemSize: CGSize(width: 200.0, height: 200.0)), transition: .immediate) } - self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItemRange: mappedTransition.stationaryItemRange), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: completion) } else { - self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItemRange: transition.stationaryItemRange), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: completion) } } } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { - self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size), indexOffset: 0), transition: .immediate), stationaryItemRange: nil), completion: { _ in }) + self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size)), transition: .immediate), stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 4461f256e7..e1868e81a7 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -90,8 +90,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) - - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(CloudFileMediaResource(location: largestSize.location, size: largestSize.size ?? 0)).start()) + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource).start()) } else { self._ready.set(.single(Void())) } @@ -190,7 +189,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile { if isVisible { - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(CloudFileMediaResource(location: file.location, size: file.size)).start()) + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) } else { self.fetchDisposable.set(nil) } diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift new file mode 100644 index 0000000000..42b3a396f6 --- /dev/null +++ b/TelegramUI/ChatInputNode.swift @@ -0,0 +1,11 @@ +import Foundation +import Display +import AsyncDisplayKit + +class ChatInputNode: ASDisplayNode { + var interfaceInteraction: ChatPanelInterfaceInteraction? + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 0.0 + } +} diff --git a/TelegramUI/ChatInputPanelNode.swift b/TelegramUI/ChatInputPanelNode.swift index 2934a34f9f..0625bcfc64 100644 --- a/TelegramUI/ChatInputPanelNode.swift +++ b/TelegramUI/ChatInputPanelNode.swift @@ -7,8 +7,8 @@ import TelegramCore class ChatInputPanelNode: ASDisplayNode { var account: Account? var interfaceInteraction: ChatPanelInterfaceInteraction? - var peer: Peer? - func updateFrames(transition: ContainedViewLayoutTransition) { + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 0.0 } } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 994d84e489..659b05c5f1 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -10,3 +10,16 @@ func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceSta } return nil } + +func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatTextInputPanelState { + switch chatPresentationInterfaceState.inputMode { + case .media: + return ChatTextInputPanelState(accessoryItems: [.keyboard]) + case .none, .text: + if chatPresentationInterfaceState.interfaceState.inputState.inputText.isEmpty { + return ChatTextInputPanelState(accessoryItems: [.stickers]) + } else { + return ChatTextInputPanelState(accessoryItems: []) + } + } +} diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift new file mode 100644 index 0000000000..43d3945d90 --- /dev/null +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -0,0 +1,21 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore + +func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction) -> ChatInputNode? { + switch chatPresentationInterfaceState.inputMode { + case .media: + if let currentNode = currentNode as? ChatMediaInputNode { + return currentNode + } else if let inputMediaNode = inputMediaNode { + return inputMediaNode + } else { + let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction) + inputNode.interfaceInteraction = interfaceInteraction + return inputNode + } + case .none, .text: + return nil + } + return nil +} diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 7268e45e93..c679a2cd96 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -7,12 +7,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { currentPanel.selectedMessageCount = selectionState.selectedIds.count currentPanel.interfaceInteraction = interfaceInteraction - currentPanel.peer = chatPresentationInterfaceState.peer return currentPanel } else { let panel = ChatMessageSelectionInputPanelNode() panel.account = account - panel.peer = chatPresentationInterfaceState.peer panel.selectedMessageCount = selectionState.selectedIds.count panel.interfaceInteraction = interfaceInteraction return panel @@ -27,12 +25,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState break case .member: if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - currentPanel.peer = peer return currentPanel } else { let panel = ChatChannelSubscriberInputPanelNode() panel.account = account - panel.peer = peer return panel } } @@ -40,12 +36,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState switch channel.participationStatus { case .kicked, .left: if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - currentPanel.peer = peer return currentPanel } else { let panel = ChatChannelSubscriberInputPanelNode() panel.account = account - panel.peer = peer return panel } case .member: @@ -56,19 +50,16 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let currentPanel = currentPanel as? ChatTextInputPanelNode { currentPanel.interfaceInteraction = interfaceInteraction - currentPanel.peer = peer return currentPanel } else { if let textInputPanelNode = textInputPanelNode { textInputPanelNode.interfaceInteraction = interfaceInteraction textInputPanelNode.account = account - textInputPanelNode.peer = peer return textInputPanelNode } else { let panel = ChatTextInputPanelNode() panel.interfaceInteraction = interfaceInteraction panel.account = account - panel.peer = peer return panel } } diff --git a/TelegramUI/ChatMediaActionSheetController.swift b/TelegramUI/ChatMediaActionSheetController.swift index 3cf65d35ae..9ad622e8e7 100644 --- a/TelegramUI/ChatMediaActionSheetController.swift +++ b/TelegramUI/ChatMediaActionSheetController.swift @@ -3,6 +3,7 @@ import Display import AsyncDisplayKit import UIKit import SwiftSignalKit +import Photos final class ChatMediaActionSheetController: ActionSheetController { private let _ready = Promise() @@ -11,6 +12,7 @@ final class ChatMediaActionSheetController: ActionSheetController { } private var didSetReady = false + var photo: (PHAsset) -> Void = { _ in } var location: () -> Void = { } var contacts: () -> Void = { } @@ -21,7 +23,12 @@ final class ChatMediaActionSheetController: ActionSheetController { self.setItemGroups([ ActionSheetItemGroup(items: [ - ChatMediaActionSheetRollItem(), + ChatMediaActionSheetRollItem(assetSelected: { [weak self] asset in + if let strongSelf = self { + self?.dismissAnimated() + strongSelf.photo(asset) + } + }), ActionSheetButtonItem(title: "File", action: {}), ActionSheetButtonItem(title: "Location", action: { [weak self] in self?.dismissAnimated() diff --git a/TelegramUI/ChatMediaActionSheetRollItem.swift b/TelegramUI/ChatMediaActionSheetRollItem.swift index c571fe44bd..fd69b44694 100644 --- a/TelegramUI/ChatMediaActionSheetRollItem.swift +++ b/TelegramUI/ChatMediaActionSheetRollItem.swift @@ -6,8 +6,14 @@ import Photos import SwiftSignalKit final class ChatMediaActionSheetRollItem: ActionSheetItem { + private let assetSelected: (PHAsset) -> Void + + init(assetSelected: @escaping (PHAsset) -> Void) { + self.assetSelected = assetSelected + } + func node() -> ActionSheetItemNode { - return ChatMediaActionSheetRollItemNode() + return ChatMediaActionSheetRollItemNode(assetSelected: self.assetSelected) } } @@ -19,7 +25,11 @@ private final class ChatMediaActionSheetRollItemNode: ActionSheetItemNode, PHPho private var assetCollection: PHAssetCollection? private var fetchResult: PHFetchResult? - override init() { + private let assetSelected: (PHAsset) -> Void + + init(assetSelected: @escaping (PHAsset) -> Void) { + self.assetSelected = assetSelected + self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) @@ -64,7 +74,11 @@ private final class ChatMediaActionSheetRollItemNode: ActionSheetItemNode, PHPho if let fetchResult = self.fetchResult { for i in 0 ..< fetchResult.count { let asset = fetchResult.object(at: i) - items.append(ActionSheetRollImageItem(asset: asset)) + items.append(ActionSheetRollImageItem(asset: asset, selected: { [weak self] in + if let strongSelf = self { + strongSelf.assetSelected(asset) + } + })) } } diff --git a/TelegramUI/ChatMediaInputGridEntries.swift b/TelegramUI/ChatMediaInputGridEntries.swift new file mode 100644 index 0000000000..6dfb0b0e6d --- /dev/null +++ b/TelegramUI/ChatMediaInputGridEntries.swift @@ -0,0 +1,39 @@ +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +struct ChatMediaInputGridEntryStableId: Hashable { + let collectionId: ItemCollectionId + let itemId: ItemCollectionItemIndex.Id + + static func ==(lhs: ChatMediaInputGridEntryStableId, rhs: ChatMediaInputGridEntryStableId) -> Bool { + return lhs.collectionId == rhs.collectionId && lhs.itemId == rhs.itemId + } + + var hashValue: Int { + return self.itemId.hashValue + } +} + +struct ChatMediaInputGridEntry: Comparable, Identifiable { + let index: ItemCollectionViewEntryIndex + let stickerItem: StickerPackItem + let stickerPackInfo: StickerPackCollectionInfo? + + var stableId: ChatMediaInputGridEntryStableId { + return ChatMediaInputGridEntryStableId(collectionId: self.index.collectionId, itemId: self.stickerItem.index.id) + } + + static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { + return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem + } + + static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { + return ChatMediaInputStickerGridItem(account: account, collectionId: self.index.collectionId, stickerPackInfo: self.stickerPackInfo, index: self.index, stickerItem: self.stickerItem, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, selected: { }) + } +} diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift new file mode 100644 index 0000000000..034d01ebfe --- /dev/null +++ b/TelegramUI/ChatMediaInputNode.swift @@ -0,0 +1,322 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +private struct ChatMediaInputPanelTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private struct ChatMediaInputGridTransition { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let updateFirstIndexInSectionOffset: Int? + let stationaryItems: GridNodeStationaryItems +} + +private func preparedChatMediaInputPanelEntryTransition(account: Account, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + + return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates) +} + +private func preparedChatMediaInputGridEntryTransition(account: Account, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition { + var stationaryItems: GridNodeStationaryItems = .none + switch update { + case .generic: + break + case .scroll: + var fromStableIds = Set() + for entry in fromEntries { + fromStableIds.insert(entry.stableId) + } + var index = 0 + var indices = Set() + for entry in toEntries { + if fromStableIds.contains(entry.stableId) { + indices.insert(index) + } + index += 1 + } + stationaryItems = .indices(indices) + } + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) } + + var firstIndexInSectionOffset = 0 + if !toEntries.isEmpty { + firstIndexInSectionOffset = Int(toEntries[0].index.itemIndex.index) + } + + return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems) +} + +private func chatMediaInputPanelEntries(view: ItemCollectionsView) -> [ChatMediaInputPanelEntry] { + var entries: [ChatMediaInputPanelEntry] = [] + var index = 0 + for (_, info, item) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem)) + index += 1 + } + } + return entries +} + +private func chatMediaInputGridEntries(view: ItemCollectionsView) -> [ChatMediaInputGridEntry] { + var entries: [ChatMediaInputGridEntry] = [] + + var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] + for (id, info, _) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + stickerPackInfos[id] = info + } + } + + for entry in view.entries { + if let item = entry.item as? StickerPackItem { + entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId])) + } + } + return entries +} + +private enum StickerPacksCollectionPosition: Equatable { + case initial + case scroll(aroundIndex: ItemCollectionViewEntryIndex) + + static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool { + switch lhs { + case .initial: + if case .initial = rhs { + return true + } else { + return false + } + case let .scroll(aroundIndex): + if case .scroll(aroundIndex) = rhs { + return true + } else { + return false + } + } + } +} + +private enum StickerPacksCollectionUpdate { + case generic + case scroll +} + +final class ChatMediaInputNodeInteraction { + var highlightedItemCollectionId: ItemCollectionId? +} + +final class ChatMediaInputNode: ChatInputNode { + private let account: Account + private let controllerInteraction: ChatControllerInteraction + + private var inputNodeInteraction: ChatMediaInputNodeInteraction! + + private let collectionListPanel: ASDisplayNode + private let collectionListSeparator: ASDisplayNode + + private let disposable = MetaDisposable() + + private let listView: ListView + private let gridNode: GridNode + + private let itemCollectionsViewPosition = Promise() + private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? + private var currentView: ItemCollectionsView? + + init(account: Account, controllerInteraction: ChatControllerInteraction) { + self.account = account + self.controllerInteraction = controllerInteraction + + self.collectionListPanel = ASDisplayNode() + self.collectionListPanel.backgroundColor = UIColor(0xF5F6F8) + + self.collectionListSeparator = ASDisplayNode() + self.collectionListSeparator.isLayerBacked = true + self.collectionListSeparator.backgroundColor = UIColor(0xBEC2C6) + + self.listView = ListView() + self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + + self.gridNode = GridNode() + + super.init() + + self.inputNodeInteraction = ChatMediaInputNodeInteraction() + + self.clipsToBounds = true + self.backgroundColor = UIColor(0xE8EBF0) + + self.addSubnode(self.collectionListPanel) + self.addSubnode(self.collectionListSeparator) + self.addSubnode(self.listView) + self.addSubnode(self.gridNode) + + let itemCollectionsView = self.itemCollectionsViewPosition.get() + |> distinctUntilChanged + |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in + switch position { + case .initial: + return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) + |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in + return (view, .generic) + } + case let .scroll(aroundIndex): + var firstTime = true + return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140) + |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in + let update: StickerPacksCollectionUpdate + if firstTime { + firstTime = false + update = .scroll + } else { + update = .generic + } + return (view, update) + } + } + } + + let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) + + let inputNodeInteraction = self.inputNodeInteraction! + + let transitions = itemCollectionsView + |> map { (view, update) -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let panelEntries = chatMediaInputPanelEntries(view: view) + let gridEntries = chatMediaInputGridEntries(view: view) + let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) + return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) + } + + self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in + if let strongSelf = self { + strongSelf.currentView = view + strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime) + strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) + } + })) + + self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in + if let strongSelf = self { + if let topVisible = visibleItems.topVisible { + if let item = topVisible.1 as? ChatMediaInputStickerGridItem { + let collectionId = item.index.collectionId + if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId { + strongSelf.inputNodeInteraction.highlightedItemCollectionId = collectionId + var selectedItemNode: ChatMediaInputStickerPackItemNode? + strongSelf.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { + itemNode.updateIsHighlighted() + if itemNode.currentCollectionId == collectionId { + strongSelf.listView.ensureItemNodeVisible(itemNode) + } + } + } + } + } + } + + if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom { + if topIndex <= 10 && currentView.lower != nil { + let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index) + if strongSelf.currentStickerPacksCollectionPosition != position { + strongSelf.currentStickerPacksCollectionPosition = position + strongSelf.itemCollectionsViewPosition.set(.single(position)) + } + } else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil { + let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (bottomItem as! ChatMediaInputStickerGridItem).index) + if strongSelf.currentStickerPacksCollectionPosition != position { + strongSelf.currentStickerPacksCollectionPosition = position + strongSelf.itemCollectionsViewPosition.set(.single(position)) + } + } + } + } + } + + self.currentStickerPacksCollectionPosition = .initial + self.itemCollectionsViewPosition.set(.single(.initial)) + } + + deinit { + self.disposable.dispose() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + let separatorHeight = UIScreenPixel + + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: 41.0))) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: separatorHeight))) + + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) + self.listView.position = CGPoint(x: width / 2.0, y: 41.0 / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: listViewCurve) + + self.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in }) + + self.gridNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: 258.0 - 41.0)) + + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: width, height: 258.0 - 41.0), insets: UIEdgeInsets(), preloadSize: 300.0, itemSize: CGSize(width: 75.0, height: 75.0)), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + return 258.0 + } + + private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) { + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else { + options.insert(.AnimateInsertion) + } + self.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in + }) + } + + private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) { + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) + } +} diff --git a/TelegramUI/ChatMediaInputPanelEntries.swift b/TelegramUI/ChatMediaInputPanelEntries.swift new file mode 100644 index 0000000000..8dfe129523 --- /dev/null +++ b/TelegramUI/ChatMediaInputPanelEntries.swift @@ -0,0 +1,70 @@ +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +enum ChatMediaInputPanelEntryStableId: Hashable { + case stickerPack(Int64) + + static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool { + switch lhs { + case let .stickerPack(id): + if case .stickerPack(id) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .stickerPack(id): + return id.hashValue + } + } +} + +enum ChatMediaInputPanelEntry: Comparable, Identifiable { + case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?) + + var stableId: ChatMediaInputPanelEntryStableId { + switch self { + case let .stickerPack(_, info, _): + return .stickerPack(info.id.id) + } + } + + static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { + switch lhs { + case let .stickerPack(index, info, topItem): + if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem { + return true + } else { + return false + } + } + } + + static func <(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { + switch lhs { + case let .stickerPack(lhsIndex, lhsInfo, _): + switch rhs { + case let .stickerPack(rhsIndex, rhsInfo, _): + if lhsIndex == rhsIndex { + return lhsInfo.id.id < rhsInfo.id.id + } else { + return lhsIndex < rhsIndex + } + } + } + } + + func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { + switch self { + case let .stickerPack(index, info, topItem): + return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, selected: { + }) + } + } +} diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift new file mode 100644 index 0000000000..2d36520cc5 --- /dev/null +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -0,0 +1,163 @@ +import Foundation +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox + +final class ChatMediaInputStickerGridSection: GridSection { + let collectionId: ItemCollectionId + let collectionInfo: StickerPackCollectionInfo? + let height: CGFloat = 26.0 + + var hashValue: Int { + return self.collectionId.hashValue + } + + init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?) { + self.collectionId = collectionId + self.collectionInfo = collectionInfo + } + + func isEqual(to: GridSection) -> Bool { + if let to = to as? ChatMediaInputStickerGridSection { + return self.collectionId == to.collectionId + } else { + return false + } + } + + func node() -> ASDisplayNode { + return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo) + } +} + +private let sectionTitleFont = Font.medium(12.0) + +final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { + let titleNode: ASTextNode + + init(collectionInfo: StickerPackCollectionInfo?) { + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.titleNode) + self.titleNode.attributedText = NSAttributedString(string: collectionInfo?.title.uppercased() ?? "", font: sectionTitleFont, textColor: UIColor(0x9099A2)) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 8.0), size: titleSize) + } +} + +final class ChatMediaInputStickerGridItem: GridItem { + let account: Account + let index: ItemCollectionViewEntryIndex + let stickerItem: StickerPackItem + let selected: () -> Void + let interfaceInteraction: ChatControllerInteraction? + let inputNodeInteraction: ChatMediaInputNodeInteraction + + let section: GridSection? + + init(account: Account, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) { + self.account = account + self.index = index + self.stickerItem = stickerItem + self.interfaceInteraction = interfaceInteraction + self.inputNodeInteraction = inputNodeInteraction + self.selected = selected + self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo) + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = ChatMediaInputStickerGridItemNode() + node.interfaceInteraction = self.interfaceInteraction + node.inputNodeInteraction = self.inputNodeInteraction + node.setup(account: self.account, stickerItem: self.stickerItem) + node.selected = self.selected + return node + } +} + +final class ChatMediaInputStickerGridItemNode: GridItemNode { + private var currentState: (Account, StickerPackItem, CGSize)? + private let imageNode: TransformImageNode + + private let stickerFetchedDisposable = MetaDisposable() + + var interfaceInteraction: ChatControllerInteraction? + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var selected: (() -> Void)? + + override init() { + self.imageNode = TransformImageNode() + + super.init() + + self.addSubnode(self.imageNode) + } + + deinit { + stickerFetchedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + } + + func setup(account: Account, stickerItem: StickerPackItem) { + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { + if let dimensions = stickerItem.file.dimensions { + self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(fileInteractiveFetched(account: account, file: stickerItem.file).start()) + + self.currentState = (account, stickerItem, dimensions) + self.setNeedsLayout() + } + } + + //self.updateSelectionState(animated: false) + //self.updateHiddenMedia() + } + + override func layout() { + super.layout() + + let imageFrame = self.bounds.insetBy(dx: 6.0, dy: 6.0) + self.imageNode.frame = imageFrame + + if let (_, _, mediaDimensions) = self.currentState { + let imageSize = mediaDimensions.aspectFitted(imageFrame.size) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets()))() + } + } + + /*func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + if self.messageId == id { + return self.imageNode + } else { + return nil + } + }*/ + + @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { + if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { + interfaceInteraction.sendSticker(item.file) + } + /*if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state { + controllerInteraction.openMessage(messageId) + }*/ + } +} diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift new file mode 100644 index 0000000000..fd109c6dc5 --- /dev/null +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -0,0 +1,115 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class ChatMediaInputStickerPackItem: ListViewItem { + let account: Account + let inputNodeInteraction: ChatMediaInputNodeInteraction + let collectionId: ItemCollectionId + let stickerPackItem: StickerPackItem? + let selectedItem: () -> Void + let index: Int + + var selectable: Bool { + return true + } + + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, stickerPackItem: StickerPackItem?, index: Int, selected: @escaping () -> Void) { + self.account = account + self.inputNodeInteraction = inputNodeInteraction + self.collectionId = collectionId + self.stickerPackItem = stickerPackItem + self.selectedItem = selected + self.index = index + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + async { + let node = ChatMediaInputStickerPackItemNode() + node.contentSize = CGSize(width: 41.0, height: 41.0) + node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.inputNodeInteraction = self.inputNodeInteraction + node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId) + completion(node, { + }) + } + } + + func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: (ListViewItemNodeLayout, () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + }) + } + + func selected(listView: ListView) { + self.selectedItem() + } +} + +private let boundingSize = CGSize(width: 41.0, height: 41.0) +private let imageSize = CGSize(width: 30.0, height: 30.0) +private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let verticalOffset: CGFloat = 3.0 + +private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2)) + +final class ChatMediaInputStickerPackItemNode: ListViewItemNode { + private let imageNode: TransformImageNode + private let highlightNode: ASImageNode + + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var currentCollectionId: ItemCollectionId? + private var currentItem: StickerPackItem? + + private let stickerFetchedDisposable = MetaDisposable() + + init() { + self.highlightNode = ASImageNode() + self.highlightNode.isLayerBacked = true + self.highlightNode.image = highlightBackground + self.highlightNode.isHidden = true + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = true + + self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) + + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) + self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.imageNode.alphaTransitionOnFirstUpdate = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.highlightNode) + self.addSubnode(self.imageNode) + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) { + self.currentCollectionId = collectionId + + if self.currentItem != item { + self.currentItem = item + + if let item = item, let dimensions = item.file.dimensions { + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: dimensions.aspectFitted(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + imageApply() + self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: item.file, small: true)) + self.stickerFetchedDisposable.set(fileInteractiveFetched(account: account, file: item.file).start()) + } + + self.updateIsHighlighted() + } + } + + func updateIsHighlighted() { + if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction { + self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId + } + } +} diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index a1d12beb75..f4643a8316 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -15,6 +15,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { private var progressNode: RadialProgressNode? private var tapRecognizer: UITapGestureRecognizer? + private var account: Account? + private var messageIdAndFlags: (MessageId, MessageFlags)? private var media: Media? private let statusDisposable = MetaDisposable() @@ -48,6 +50,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Fetching: + if let account = self.account, let (messageId, flags) = self.messageIdAndFlags, flags.contains(.Unsent) && !flags.contains(.Failed) { + account.postbox.modify({ modifier -> Void in + modifier.deleteMessages([messageId]) + }).start() + } if let cancel = self.fetchControls.with({ return $0?.cancel }) { cancel() } @@ -75,11 +82,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + let currentMessageIdAndFlags = self.messageIdAndFlags let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() - return { account, media, corners, automaticDownload, constrainedSize in + return { account, message, media, corners, automaticDownload, constrainedSize in var initialBoundingSize: CGSize var nativeSize: CGSize @@ -113,10 +121,15 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { mediaUpdated = true } + var statusUpdated = mediaUpdated + if currentMessageIdAndFlags?.0 != message.id || currentMessageIdAndFlags?.1 != message.flags { + statusUpdated = true + } + if mediaUpdated { if let image = media as? TelegramMediaImage { updateImageSignal = chatMessagePhoto(account: account, photo: image) - updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) + updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) @@ -126,7 +139,6 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { }) } else if let file = media as? TelegramMediaFile { updateImageSignal = chatMessageVideo(account: account, video: file) - updatedStatusSignal = chatMessageFileStatus(account: account, file: file) updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start()) @@ -137,6 +149,32 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } + if statusUpdated { + if let image = media as? TelegramMediaImage { + if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus { + return .Fetching(progress: pendingStatus.progress) + } else { + return resourceStatus + } + } + } else { + updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) + } + } else if let file = media as? TelegramMediaFile { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus { + return .Fetching(progress: pendingStatus.progress) + } else { + return resourceStatus + } + } + } + } + let arguments = TransformImageArguments(corners: corners, imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) @@ -153,6 +191,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { return (CGSize(width: adjustedImageSize.width, height: adjustedImageSize.height), { [weak self] in if let strongSelf = self { + strongSelf.account = account + strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.media = media strongSelf.imageNode.frame = adjustedImageFrame strongSelf.progressNode?.position = CGPoint(x: adjustedImageFrame.midX, y: adjustedImageFrame.midY) @@ -225,12 +265,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, media, corners, automaticDownload, constrainedSize in + return { account, message, media, corners, automaticDownload, constrainedSize in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var imageLayout: (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -240,7 +280,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (initialWidth, continueLayout) = imageLayout(account, media, corners, automaticDownload, constrainedSize) + let (initialWidth, continueLayout) = imageLayout(account, message, media, corners, automaticDownload, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 7a67328354..15585167bf 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -50,7 +50,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (initialWidth, refineLayout) = interactiveImageLayout(item.account, selectedMedia!, imageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhoto, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveImageLayout(item.account, item.message, selectedMedia!, imageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhoto, CGSize(width: constrainedSize.width, height: constrainedSize.height)) return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index d4bece6e70..d1be24e02a 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -8,32 +8,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: UIButton private let forwardButton: UIButton - override var peer: Peer? { - didSet { - var canDelete = false - if let channel = self.peer as? TelegramChannel { - switch channel.info { - case .broadcast: - switch channel.role { - case .creator, .editor, .moderator: - canDelete = true - case .member: - canDelete = false - } - case .group: - switch channel.role { - case .creator, .editor, .moderator: - canDelete = true - case .member: - canDelete = false - } - } - } else { - canDelete = true - } - self.deleteButton.isHidden = !canDelete - } - } + private var presentationInterfaceState = ChatPresentationInterfaceState() var selectedMessageCount: Int = 0 { didSet { @@ -63,19 +38,6 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 45.0) - } - - override func layout() { - super.layout() - - let bounds = self.bounds - - self.deleteButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 53.0, height: 45.0)) - self.forwardButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 45.0)) - } - @objc func deleteButtonPressed() { self.interfaceInteraction?.deleteSelectedMessages() } @@ -83,4 +45,38 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { @objc func forwardButtonPressed() { self.interfaceInteraction?.forwardSelectedMessages() } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + + var canDelete = false + if let channel = interfaceState.peer as? TelegramChannel { + switch channel.info { + case .broadcast: + switch channel.role { + case .creator, .editor, .moderator: + canDelete = true + case .member: + canDelete = false + } + case .group: + switch channel.role { + case .creator, .editor, .moderator: + canDelete = true + case .member: + canDelete = false + } + } + } else { + canDelete = true + } + self.deleteButton.isHidden = !canDelete + } + + self.deleteButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 53.0, height: 47.0)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + + return 47.0 + } } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index ecadc2f20c..b56945f0be 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -39,7 +39,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if self.telegramFile != telegramFile { self.telegramFile = telegramFile - let signal = chatMessageSticker(account: item.account, file: telegramFile) + let signal = chatMessageSticker(account: item.account, file: telegramFile, small: false) self.imageNode.setSignal(account: item.account, signal: signal) self.fetchDisposable.set(fileInteractiveFetched(account: item.account, file: telegramFile).start()) } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 248112e129..1e87b51a0a 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -123,7 +123,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let file = webpage.file { if file.isVideo { - let (initialImageWidth, refineLayout) = contentImageLayout(item.account, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { @@ -132,7 +132,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } else if let image = webpage.image { if let type = webpage.type, ["photo"].contains(type) { - let (initialImageWidth, refineLayout) = contentImageLayout(item.account, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 07e00d6164..f8854d6e13 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -7,12 +7,14 @@ final class ChatPanelInterfaceInteraction { let deleteSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void let updateTextInputState: (ChatTextInputState) -> Void + let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void - init(setupReplyMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void) { + init(setupReplyMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void) { self.setupReplyMessage = setupReplyMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages self.updateTextInputState = updateTextInputState + self.updateInputMode = updateInputMode } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 025c1e42e3..749c9a83e9 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -6,21 +6,33 @@ enum ChatPresentationInputContext { case mention } +enum ChatInputMode { + case none + case text + case media +} + struct ChatPresentationInterfaceState: Equatable { let interfaceState: ChatInterfaceState let peer: Peer? + let inputTextPanelState: ChatTextInputPanelState let inputContext: ChatPresentationInputContext? + let inputMode: ChatInputMode init() { self.interfaceState = ChatInterfaceState() + self.inputTextPanelState = ChatTextInputPanelState() self.peer = nil self.inputContext = nil + self.inputMode = .none } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputContext: ChatPresentationInputContext?) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputContext: ChatPresentationInputContext?, inputMode: ChatInputMode) { self.interfaceState = interfaceState self.peer = peer + self.inputTextPanelState = inputTextPanelState self.inputContext = inputContext + self.inputMode = inputMode } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -35,22 +47,38 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.inputTextPanelState != rhs.inputTextPanelState { + return false + } + if lhs.inputContext != rhs.inputContext { return false } + if lhs.inputMode != rhs.inputMode { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputContext: self.inputContext) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputContext: self.inputContext, inputMode: self.inputMode) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputContext: self.inputContext) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputContext: self.inputContext, inputMode: self.inputMode) } func updatedInputContext(_ f: (ChatPresentationInputContext?) -> ChatPresentationInputContext?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputContext: f(self.inputContext)) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputContext: f(self.inputContext), inputMode: self.inputMode) + } + + func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputContext: self.inputContext, inputMode: self.inputMode) + } + + func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputContext: self.inputContext, inputMode: f(self.inputMode)) } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 06a5d0765f..adbcd44844 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -6,14 +6,14 @@ import Postbox import TelegramCore private let textInputViewBackground: UIImage = { - let diameter: CGFloat = 10.0 + let diameter: CGFloat = 35.0 UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0) let context = UIGraphicsGetCurrentContext()! - context.setFillColor(UIColor(0xfafafa).cgColor) + context.setFillColor(UIColor(0xF5F6F8).cgColor) context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setStrokeColor(UIColor(0xc7c7cc).cgColor) + context.setStrokeColor(UIColor(0xC9CDD1).cgColor) let strokeWidth: CGFloat = 0.5 context.setLineWidth(strokeWidth) context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) @@ -23,16 +23,75 @@ private let textInputViewBackground: UIImage = { return image }() -private let attachmentIcon = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.precomposed() +private let attachmentIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: UIColor(0x9099A2)) +private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: UIColor(0x9099A2)) +private let sendIcon = UIImage(bundleImageName: "Chat/Input/Text/IconSend")?.precomposed() + +enum ChatTextInputAccessoryItem { + case keyboard + case stickers + case inputButtons +} + +struct ChatTextInputPanelState: Equatable { + let accessoryItems: [ChatTextInputAccessoryItem] + + init(accessoryItems: [ChatTextInputAccessoryItem]) { + self.accessoryItems = accessoryItems + } + + init() { + self.accessoryItems = [] + } + + static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool { + if lhs.accessoryItems != rhs.accessoryItems { + return false + } + return true + } +} + +private let keyboardImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard")?.precomposed() +private let stickersImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.precomposed() +private let inputButtonsImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons")?.precomposed() + +private final class AccessoryItemIconButton: UIButton { + init(item: ChatTextInputAccessoryItem) { + super.init(frame: CGRect()) + + switch item { + case .keyboard: + self.setImage(keyboardImage, for: []) + case .stickers: + self.setImage(stickersImage, for: []) + case .inputButtons: + self.setImage(inputButtonsImage, for: []) + } + + //self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var buttonWidth: CGFloat { + return (self.image(for: [])?.size.width ?? 0.0) + CGFloat(8.0) + } +} class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: TextNode var textInputNode: ASEditableTextNode? let textInputBackgroundView: UIImageView + let micButton: UIButton let sendButton: UIButton let attachmentButton: UIButton + private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] + var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } var updateHeight: () -> Void = { } @@ -40,25 +99,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var updatingInputState = false private var currentPlaceholder: String? - override var peer: Peer? { - didSet { - if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) { - let placeholder: String - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - placeholder = "Broadcast" - } else { - placeholder = "Message" - } - if self.currentPlaceholder != placeholder { - self.currentPlaceholder = placeholder - let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) - self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) - let _ = placeholderApply() - } - } - } - } + + private var presentationInterfaceState = ChatPresentationInterfaceState() var inputTextState: ChatTextInputState { get { @@ -72,7 +114,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } set(value) { if let textInputNode = self.textInputNode { self.updatingInputState = true - textInputNode.attributedText = NSAttributedString(string: value.inputText, font: Font.regular(16.0), textColor: UIColor.black) + textInputNode.attributedText = NSAttributedString(string: value.inputText, font: Font.regular(17.0), textColor: UIColor.black) textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count) self.updatingInputState = false } @@ -84,19 +126,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return self.textInputNode?.attributedText?.string ?? "" } set(value) { if let textInputNode = self.textInputNode { - textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(16.0), textColor: UIColor.black) + textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(17.0), textColor: UIColor.black) self.editableTextNodeDidUpdateText(textInputNode) } } } - let textFieldInsets = UIEdgeInsets(top: 9.0, left: 41.0, bottom: 8.0, right: 0.0) - let textInputViewInternalInsets = UIEdgeInsets(top: 4.0, left: 5.0, bottom: 4.0, right: 5.0) + let textFieldInsets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0) + let textInputViewInternalInsets = UIEdgeInsets(top: 6.5, left: 13.0, bottom: 7.5, right: 13.0) override init() { self.textInputBackgroundView = UIImageView(image: textInputViewBackground) self.textPlaceholderNode = TextNode() + self.textPlaceholderNode.isLayerBacked = true self.attachmentButton = UIButton() + self.micButton = UIButton() self.sendButton = UIButton() super.init() @@ -105,24 +149,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) - self.sendButton.titleLabel?.font = Font.medium(17.0) - self.sendButton.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 6.0, bottom: 8.0, right: 6.0) - self.sendButton.setTitleColor(UIColor(0x007ee5), for: []) - self.sendButton.setTitleColor(UIColor.gray, for: [.highlighted]) - self.sendButton.setTitle("Send", for: []) - self.sendButton.sizeToFit() + self.micButton.setImage(micIcon, for: []) + self.micButton.addTarget(self, action: #selector(self.micButtonPressed), for: .touchUpInside) + self.view.addSubview(self.micButton) + + self.sendButton.setImage(sendIcon, for: []) self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) + self.sendButton.alpha = 0.0 + self.view.addSubview(self.sendButton) self.view.addSubview(self.textInputBackgroundView) let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(16.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(17.0), textColor: UIColor(0xC8C8CE)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) self.textPlaceholderNode.frame = CGRect(origin: CGPoint(), size: placeholderSize.size) let _ = placeholderApply() self.addSubnode(self.textPlaceholderNode) - self.view.addSubview(self.sendButton) - self.textInputBackgroundView.clipsToBounds = true let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in @@ -140,16 +183,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private func loadTextInputNode() { let textInputNode = ASEditableTextNode() - textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)] + textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] textInputNode.clipsToBounds = true textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.addSubnode(textInputNode) self.textInputNode = textInputNode - let sendButtonSize = self.sendButton.bounds.size - - textInputNode.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: self.frame.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: self.frame.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom) + textInputNode.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: self.frame.size.width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: self.frame.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom) self.textInputBackgroundView.isUserInteractionEnabled = false self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0]) @@ -163,55 +204,168 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.view.addGestureRecognizer(recognizer) } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let sendButtonSize = self.sendButton.bounds.size + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + let previousState = self.presentationInterfaceState + self.presentationInterfaceState = interfaceState + + if let peer = interfaceState.peer, previousState.peer == nil || !peer.isEqual(previousState.peer!) { + let placeholder: String + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + placeholder = "Broadcast" + } else { + placeholder = "Message" + } + if self.currentPlaceholder != placeholder { + self.currentPlaceholder = placeholder + let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) + self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) + let _ = placeholderApply() + } + } + } + + let minimalHeight: CGFloat = 47.0 + let minimalInputHeight: CGFloat = 35.0 + let accessoryButtonSpacing: CGFloat = 0.0 + let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel + + var animatedTransition = true + if case .immediate = transition { + animatedTransition = false + } + + var updateAccessoryButtons = false + if self.presentationInterfaceState.inputTextPanelState.accessoryItems.count == self.accessoryItemButtons.count { + for i in 0 ..< self.presentationInterfaceState.inputTextPanelState.accessoryItems.count { + if self.presentationInterfaceState.inputTextPanelState.accessoryItems[i] != self.accessoryItemButtons[i].0 { + updateAccessoryButtons = true + break + } + } + } else { + updateAccessoryButtons = true + } + + var removeAccessoryButtons: [AccessoryItemIconButton]? + if updateAccessoryButtons { + var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] + for item in self.presentationInterfaceState.inputTextPanelState.accessoryItems { + var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)? + for i in 0 ..< self.accessoryItemButtons.count { + if self.accessoryItemButtons[i].0 == item { + itemAndButton = self.accessoryItemButtons[i] + self.accessoryItemButtons.remove(at: i) + break + } + } + if itemAndButton == nil { + let button = AccessoryItemIconButton(item: item) + button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside]) + itemAndButton = (item, button) + } + updatedButtons.append(itemAndButton!) + } + for (_, button) in self.accessoryItemButtons { + if animatedTransition { + if removeAccessoryButtons == nil { + removeAccessoryButtons = [] + } + removeAccessoryButtons!.append(button) + } else { + button.removeFromSuperview() + } + } + self.accessoryItemButtons = updatedButtons + } + + var accessoryButtonsWidth: CGFloat = 0.0 + var firstButton = true + for (_, button) in self.accessoryItemButtons { + if firstButton { + firstButton = false + accessoryButtonsWidth += accessoryButtonInset + } else { + accessoryButtonsWidth += accessoryButtonSpacing + } + accessoryButtonsWidth += button.buttonWidth + } + let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { - textFieldHeight = min(115.0, max(20.0, ceil(textInputNode.measure(CGSize(width: constrainedSize.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: constrainedSize.height)).height))) + textFieldHeight = min(115.0, max(21.0, ceil(textInputNode.measure(CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))) } else { - textFieldHeight = 20.0 + textFieldHeight = 21.0 } - return CGSize(width: constrainedSize.width, height: textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom) - } - - override var frame: CGRect { - get { - return super.frame - } set(value) { - super.frame = value - } - } - - override func updateFrames(transition: ContainedViewLayoutTransition) { - let bounds = self.bounds + let panelHeight = textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom - let sendButtonSize = self.sendButton.bounds.size - let minimalHeight: CGFloat = 45.0 - transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(x: bounds.size.width - sendButtonSize.width, y: bounds.height - minimalHeight + floor((minimalHeight - sendButtonSize.height) / 2.0), width: sendButtonSize.width, height: sendButtonSize.height)) - - transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: bounds.height - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) + transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) if let textInputNode = self.textInputNode { - transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: bounds.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: bounds.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) } transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5), size: self.textPlaceholderNode.frame.size)) - transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: bounds.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width, height: bounds.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom)) + transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: width - self.textFieldInsets.left - self.textFieldInsets.right, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) + + var nextButtonTopRight = CGPoint(x: width - self.textFieldInsets.right - accessoryButtonInset, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight) + for (_, button) in self.accessoryItemButtons.reversed() { + let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) + if button.superview == nil { + self.view.addSubview(button) + button.frame = buttonFrame + transition.updateFrame(layer: button.layer, frame: buttonFrame) + if animatedTransition { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + } + } else { + transition.updateFrame(layer: button.layer, frame: buttonFrame) + } + nextButtonTopRight.x -= buttonSize.width + nextButtonTopRight.x -= accessoryButtonSpacing + } + + if let removeAccessoryButtons = removeAccessoryButtons { + for button in removeAccessoryButtons { + let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight), size: button.frame.size) + transition.updateFrame(layer: button.layer, frame: buttonFrame) + button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in + button?.removeFromSuperview() + }) + } + } + + return panelHeight } @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode { self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0 - let constrainedSize = CGSize(width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude) - let sendButtonSize = self.sendButton.bounds.size - - let textFieldHeight: CGFloat = min(115.0, max(20.0, ceil(textInputNode.measure(CGSize(width: constrainedSize.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: constrainedSize.height)).height))) - if abs(textFieldHeight - textInputNode.frame.size.height) > CGFloat(FLT_EPSILON) { - self.invalidateCalculatedLayout() - self.updateHeight() + if let text = self.textInputNode?.attributedText, text.length != 0 { + if self.sendButton.alpha.isZero { + self.sendButton.alpha = 1.0 + self.micButton.alpha = 0.0 + self.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } else { + if self.micButton.alpha.isZero { + self.micButton.alpha = 1.0 + self.sendButton.alpha = 0.0 + self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } self.interfaceInteraction?.updateTextInputState(self.inputTextState) @@ -224,6 +378,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.interfaceInteraction?.updateInputMode({ _ in .text }) + } + + @objc func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + /*self.interfaceInteraction?.updateInputMode({ mode in + if case .text = mode { + return .none + } else { + return mode + } + })*/ + } + @objc func sendButtonPressed() { let text = self.textInputNode?.attributedText?.string ?? "" if !text.isEmpty { @@ -235,12 +403,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.displayAttachmentMenu() } + @objc func micButtonPressed() { + } + @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.ensureFocused() } } + var isFocused: Bool { + return self.textInputNode?.isFirstResponder() ?? false + } + func ensureUnfocused() { self.textInputNode?.resignFirstResponder() } @@ -267,11 +442,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { }*/ } - /*override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView! { - if let textInputNode = self.textInputNode where self.textInputBackgroundView.frame.contains(point) { - return textInputNode.view + @objc func accessoryItemButtonPressed(_ button: UIView) { + for (item, currentButton) in self.accessoryItemButtons { + if currentButton === button { + switch item { + case .inputButtons: + break + case .stickers: + self.interfaceInteraction?.updateInputMode({ _ in .media }) + case .keyboard: + self.interfaceInteraction?.updateInputMode({ _ in .text }) + } + break + } } - - return super.hitTest(point, withEvent: event) - }*/ + } } diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index 928c3c8af9..4024b37c27 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -121,7 +121,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { /*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) self.videoNode.player = VideoPlayer(source: source)*/ - let player = MediaPlayer(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) + let player = MediaPlayer(account: account, resource: file.resource) player.attachPlayerNode(self.videoNode) self.player = player self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index 5085b81623..03536db8b3 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -263,6 +263,7 @@ public class ContactsController: ViewController { let interaction = ContactsControllerInteraction(openPeer: { [weak self] peerId in if let strongSelf = self { + strongSelf.contactsNode.listView.clearHighlightAnimated(true) (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) } }, activateSearch: { [weak self] in diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 3acf9a9084..f17f3300f0 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -80,8 +80,10 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa var fetchedCount: Int32 = 0 - let readCount = min(resource.size - context.readingOffset, Int(bufferSize)) - let data = account.postbox.mediaBox.resourceData(resource, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) + let resourceSize: Int = resource.size ?? 0 + + let readCount = min(resourceSize - context.readingOffset, Int(bufferSize)) + let data = account.postbox.mediaBox.resourceData(resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) var fetchedData: Data? let semaphore = DispatchSemaphore(value: 0) let _ = data.start(next: { data in @@ -110,18 +112,20 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe var result: Int64 = offset + let resourceSize: Int = resource.size ?? 0 + if (whence & AVSEEK_SIZE) != 0 { - result = Int64(resource.size) + result = Int64(resourceSize) } else { - context.readingOffset = Int(min(Int64(resource.size), offset)) + context.readingOffset = Int(min(Int64(resourceSize), offset)) if context.readingOffset != context.requestedDataOffset { context.requestedDataOffset = context.readingOffset - if context.readingOffset >= resource.size { + if context.readingOffset >= resourceSize { context.fetchedDataDisposable.set(nil) } else { - context.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, in: context.readingOffset ..< resource.size).start()) + context.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: context.readingOffset ..< resourceSize).start()) } } } @@ -168,7 +172,9 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.account = account self.resource = resource - self.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, in: 0 ..< resource.size).start()) + let resourceSize: Int = resource.size ?? 0 + + self.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: 0 ..< resourceSize).start()) var avFormatContextRef = avformat_alloc_context() guard let avFormatContext = avFormatContextRef else { diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift new file mode 100644 index 0000000000..b29514fa47 --- /dev/null +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -0,0 +1,90 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore +import ImageIO +import MobileCoreServices +import Display +import UIKit + +public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedMediaResourceRepresentation) -> Signal { + if let representation = representation as? CachedStickerAJpegRepresentation { + return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + } + return .never() +} + +private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedStickerAJpegRepresentation) -> Signal { + return Signal({ subscriber in + if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + if let image = UIImage.convert(fromWebP: data) { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + let colorData = NSMutableData() + let alphaData = NSMutableData() + + let size = representation.size ?? CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + + let colorImage: UIImage + if let _ = representation.size { + colorImage = generateImage(size, contextGenerator: { size, context in + context.setBlendMode(.copy) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }, scale: 1.0)! + } else { + colorImage = image + } + + let alphaImage = generateImage(size, contextGenerator: { size, context in + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.clip(to: CGRect(origin: CGPoint(), size: size), mask: colorImage.cgImage!) + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }, scale: 1.0) + + if let alphaImage = alphaImage, let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypeJPEG, 1, nil), let alphaDestination = CGImageDestinationCreateWithData(alphaData as CFMutableData, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, nil) + CGImageDestinationSetProperties(alphaDestination, nil) + + let colorQuality: Float + let alphaQuality: Float + if representation.size == nil { + colorQuality = 0.6 + alphaQuality = 0.6 + } else { + colorQuality = 0.5 + alphaQuality = 0.4 + } + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + let optionsAlpha = NSMutableDictionary() + optionsAlpha.setObject(alphaQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, colorImage.cgImage!, options as CFDictionary) + CGImageDestinationAddImage(alphaDestination, alphaImage.cgImage!, optionsAlpha as CFDictionary) + if CGImageDestinationFinalize(colorDestination) && CGImageDestinationFinalize(alphaDestination) { + let finalData = NSMutableData() + var colorSize: Int32 = Int32(colorData.length) + finalData.append(&colorSize, length: 4) + finalData.append(colorData as Data) + var alphaSize: Int32 = Int32(alphaData.length) + finalData.append(&alphaSize, length: 4) + finalData.append(alphaData as Data) + + let _ = try? finalData.write(to: url, options: [.atomic]) + + subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + }) |> runOn(account.graphicsThreadPool) +} diff --git a/TelegramUI/FileResources.swift b/TelegramUI/FileResources.swift index f61135c075..52f9915c1c 100644 --- a/TelegramUI/FileResources.swift +++ b/TelegramUI/FileResources.swift @@ -3,14 +3,10 @@ import Postbox import SwiftSignalKit import TelegramCore -func fileResource(_ file: TelegramMediaFile) -> CloudFileMediaResource { - return CloudFileMediaResource(location: file.location, size: file.size) -} - func fileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal { - return account.postbox.mediaBox.fetchedResource(fileResource(file)) + return account.postbox.mediaBox.fetchedResource(file.resource) } func fileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) { - account.postbox.mediaBox.cancelInteractiveResourceFetch(fileResource(file)) + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index b9100e8f70..03321bd051 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -145,7 +145,7 @@ class GalleryController: ViewController { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.donePressed)) - self.statusBar.style = .White + self.statusBar.statusBarStyle = .White let message = account.postbox.messageAtId(messageId) @@ -204,7 +204,7 @@ class GalleryController: ViewController { if let strongSelf = self { switch style { case .dark: - strongSelf.statusBar.style = .White + strongSelf.statusBar.statusBarStyle = .White strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5) strongSelf.navigationBar.stripeColor = UIColor.clear strongSelf.navigationBar.foregroundColor = UIColor.white @@ -212,7 +212,7 @@ class GalleryController: ViewController { strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true case .light: - strongSelf.statusBar.style = .Black + strongSelf.statusBar.statusBarStyle = .Black strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) strongSelf.navigationBar.foregroundColor = UIColor.black strongSelf.navigationBar.accentColor = UIColor(0x007ee5) diff --git a/TelegramUI/GridHoleItem.swift b/TelegramUI/GridHoleItem.swift index 9b3974c36d..5acebe20cf 100644 --- a/TelegramUI/GridHoleItem.swift +++ b/TelegramUI/GridHoleItem.swift @@ -2,7 +2,9 @@ import Foundation import Display import AsyncDisplayKit -class GridHoleItem: GridItem { +final class GridHoleItem: GridItem { + let section: GridSection? = nil + func node(layout: GridNodeLayout) -> GridItemNode { return GridHoleItemNode() } diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index cc6fde7f78..17fa719640 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -26,6 +26,8 @@ final class GridMessageItem: GridItem { private let message: Message private let controllerInteraction: ChatControllerInteraction + let section: GridSection? = nil + init(account: Account, message: Message, controllerInteraction: ChatControllerInteraction) { self.account = account self.message = message diff --git a/TelegramUI/GroupInfoEntries.swift b/TelegramUI/GroupInfoEntries.swift index 3f2a255521..30d27b4f91 100644 --- a/TelegramUI/GroupInfoEntries.swift +++ b/TelegramUI/GroupInfoEntries.swift @@ -236,7 +236,7 @@ enum GroupInfoEntry: PeerInfoEntry { func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { switch self { case let .info(peer, cachedData): - return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .blocks) + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, editingState: nil, sectionId: self.section.rawValue, style: .blocks) case .setGroupPhoto: return PeerInfoActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .blocks, action: { }) @@ -248,7 +248,7 @@ enum GroupInfoEntry: PeerInfoEntry { label = "Enabled" } return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .blocks, action: { - interaction.changeNotificationNoteSettings() + interaction.changeNotificationMuteSettings() }) case .sharedMedia: return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .blocks, action: { @@ -280,7 +280,7 @@ enum GroupInfoEntry: PeerInfoEntry { } } -func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { +func groupInfoEntries(view: PeerView) -> PeerInfoEntries { var entries: [PeerInfoEntry] = [] entries.append(GroupInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) @@ -400,5 +400,5 @@ func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { } } - return entries + return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil) } diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index d609da203f..68a169430c 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -20,36 +20,52 @@ private let roundCorners = { () -> UIImage in }() func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { - if let location = peer.smallProfileImage?.location.cloudLocation { - return deferred { () -> Signal in - return cachedCloudFileLocation(location) - |> `catch` { _ in - return multipartDownloadFromCloudLocation(account: account, location: location, size: nil) - |> afterNext { data in - cacheCloudFileLocation(location, data: data) + if let smallProfileImage = peer.smallProfileImage { + let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) + let imageData = resourceData + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + } else { + return Signal { subscriber in + let resourceDataDisposable = resourceData.start(next: { data in + if data.complete { + subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + subscriber.putCompletion() + } + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + let fetchedDataDisposable = account.postbox.mediaBox.fetchedResource(smallProfileImage.resource).start() + return ActionDisposable { + resourceDataDisposable.dispose() + fetchedDataDisposable.dispose() } - } - |> runOn(account.graphicsThreadPool) |> deliverOn(account.graphicsThreadPool) - |> map { data -> UIImage in - assertNotOnMainThread() - - if let image = generateImage(displayDimensions, contextGenerator: { size, context -> Void in - if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { - context.setBlendMode(.copy) - context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions)) - context.setBlendMode(.destinationOut) - context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions)) - } - }) { - return image - } else { - UIGraphicsBeginImageContextWithOptions(displayDimensions, false, 0.0) - let image = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - return image } } - } |> runOn(account.graphicsThreadPool) + } + return imageData + |> deliverOn(account.graphicsThreadPool) + |> map { data -> UIImage in + if let data = data, let image = generateImage(displayDimensions, contextGenerator: { size, context -> Void in + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + context.setBlendMode(.copy) + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions)) + context.setBlendMode(.destinationOut) + context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions)) + } + }) { + return image + } else { + UIGraphicsBeginImageContextWithOptions(displayDimensions, false, 0.0) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return image + } + } } else { return nil } diff --git a/TelegramUI/PeerInfoActionItem.swift b/TelegramUI/PeerInfoActionItem.swift index 6d1cf01b9e..f7d7f0a068 100644 --- a/TelegramUI/PeerInfoActionItem.swift +++ b/TelegramUI/PeerInfoActionItem.swift @@ -247,16 +247,10 @@ class PeerInfoActionItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/TelegramUI/PeerInfoAvatarAndNameItem.swift b/TelegramUI/PeerInfoAvatarAndNameItem.swift index 70767abe8f..91e8116890 100644 --- a/TelegramUI/PeerInfoAvatarAndNameItem.swift +++ b/TelegramUI/PeerInfoAvatarAndNameItem.swift @@ -5,17 +5,25 @@ import Postbox import TelegramCore import SwiftSignalKit +struct PeerInfoAvatarAndNameItemEditingState: Equatable { + static func ==(lhs: PeerInfoAvatarAndNameItemEditingState, rhs: PeerInfoAvatarAndNameItemEditingState) -> Bool { + return true + } +} + class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { let account: Account let peer: Peer? let cachedData: CachedPeerData? + let editingState: PeerInfoAvatarAndNameItemEditingState? let sectionId: PeerInfoItemSectionId let style: PeerInfoListStyle - init(account: Account, peer: Peer?, cachedData: CachedPeerData?, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle) { + init(account: Account, peer: Peer?, cachedData: CachedPeerData?, editingState: PeerInfoAvatarAndNameItemEditingState?, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle) { self.account = account self.peer = peer self.cachedData = cachedData + self.editingState = editingState self.sectionId = sectionId self.style = style } @@ -29,13 +37,17 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + apply(false) }) } } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { if let node = node as? PeerInfoAvatarAndNameItemNode { + var animated = true + if case .None = animation { + animated = false + } Queue.mainQueue().async { let makeLayout = node.asyncLayout() @@ -43,7 +55,7 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) Queue.mainQueue().async { completion(layout, { - apply() + apply(animated) }) } } @@ -70,6 +82,10 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { private let nameNode: TextNode private let statusNode: TextNode + private var inputSeparator: ASDisplayNode? + private var inputFirstField: ASEditableTextNode? + private var inputSecondField: ASEditableTextNode? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -102,7 +118,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let layoutNameNode = TextNode.asyncLayout(self.nameNode) let layoutStatusNode = TextNode.asyncLayout(self.statusNode) @@ -161,7 +177,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - return (layout, { [weak self] in + return (layout, { [weak self] animated in if let strongSelf = self { let avatarOriginY: CGFloat switch item.style { @@ -220,6 +236,99 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { strongSelf.nameNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size) + + if let editingState = item.editingState { + if let user = item.peer as? TelegramUser { + if strongSelf.inputSeparator == nil { + let inputSeparator = ASDisplayNode() + inputSeparator.backgroundColor = UIColor(0xc8c7cc) + inputSeparator.isLayerBacked = true + strongSelf.addSubnode(inputSeparator) + strongSelf.inputSeparator = inputSeparator + } + + if strongSelf.inputFirstField == nil { + let inputFirstField = ASEditableTextNode() + inputFirstField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + //inputFirstField.backgroundColor = UIColor.lightGray + inputFirstField.attributedPlaceholderText = NSAttributedString(string: "First Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) + inputFirstField.attributedText = NSAttributedString(string: user.firstName ?? "", font: Font.regular(17.0), textColor: UIColor.black) + strongSelf.inputFirstField = inputFirstField + strongSelf.view.addSubnode(inputFirstField) + } + + if strongSelf.inputSecondField == nil { + let inputSecondField = ASEditableTextNode() + inputSecondField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + //inputSecondField.backgroundColor = UIColor.lightGray + inputSecondField.attributedPlaceholderText = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) + inputSecondField.attributedText = NSAttributedString(string: user.lastName ?? "", font: Font.regular(17.0), textColor: UIColor.black) + strongSelf.inputSecondField = inputSecondField + strongSelf.view.addSubnode(inputSecondField) + } + + strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: 100.0, y: 49.0), size: CGSize(width: width - 100.0, height: separatorHeight)) + strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: 111.0, y: 16.0), size: CGSize(width: width - 111.0 - 8.0, height: 30.0)) + strongSelf.inputSecondField?.frame = CGRect(origin: CGPoint(x: 111.0, y: 59.0), size: CGSize(width: width - 111.0 - 8.0, height: 30.0)) + + if animated { + strongSelf.inputSeparator?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + strongSelf.inputFirstField?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + strongSelf.inputSecondField?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + + if animated { + strongSelf.statusNode.layer.animateAlpha(from: CGFloat(strongSelf.statusNode.layer.opacity), to: 0.0, duration: 0.3) + strongSelf.statusNode.alpha = 0.0 + strongSelf.nameNode.layer.animateAlpha(from: CGFloat(strongSelf.nameNode.layer.opacity), to: 0.0, duration: 0.3) + strongSelf.nameNode.alpha = 0.0 + } else { + strongSelf.statusNode.alpha = 0.0 + strongSelf.nameNode.alpha = 0.0 + } + } else { + if let inputSeparator = strongSelf.inputSeparator { + strongSelf.inputSeparator = nil + + if animated { + inputSeparator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak inputSeparator] _ in + inputSeparator?.removeFromSupernode() + }) + } else { + inputSeparator.removeFromSupernode() + } + } + if let inputFirstField = strongSelf.inputFirstField { + strongSelf.inputFirstField = nil + if animated { + inputFirstField.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak inputFirstField] _ in + inputFirstField?.removeFromSupernode() + }) + } else { + inputFirstField.removeFromSupernode() + } + } + if let inputSecondField = strongSelf.inputSecondField { + strongSelf.inputSecondField = nil + if animated { + inputSecondField.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak inputSecondField] _ in + inputSecondField?.removeFromSupernode() + }) + } else { + inputSecondField.removeFromSupernode() + } + } + if animated { + strongSelf.statusNode.layer.animateAlpha(from: CGFloat(strongSelf.statusNode.layer.opacity), to: 1.0, duration: 0.3) + strongSelf.statusNode.alpha = 1.0 + strongSelf.nameNode.layer.animateAlpha(from: CGFloat(strongSelf.nameNode.layer.opacity), to: 1.0, duration: 0.3) + strongSelf.nameNode.alpha = 1.0 + } else { + strongSelf.statusNode.alpha = 1.0 + strongSelf.nameNode.alpha = 1.0 + } + } } }) } diff --git a/TelegramUI/PeerInfoController.swift b/TelegramUI/PeerInfoController.swift index 476dfaab09..1e356b8386 100644 --- a/TelegramUI/PeerInfoController.swift +++ b/TelegramUI/PeerInfoController.swift @@ -5,13 +5,15 @@ import SwiftSignalKit import TelegramCore final class PeerInfoControllerInteraction { + let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void let openSharedMedia: () -> Void - let changeNotificationNoteSettings: () -> Void + let changeNotificationMuteSettings: () -> Void let openPeerInfo: (PeerId) -> Void - init(openSharedMedia: @escaping () -> Void, changeNotificationNoteSettings: @escaping () -> Void, openPeerInfo: @escaping (PeerId) -> Void) { + init(updateState: @escaping ((PeerInfoState?) -> PeerInfoState?) -> Void, openSharedMedia: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openPeerInfo: @escaping (PeerId) -> Void) { + self.updateState = updateState self.openSharedMedia = openSharedMedia - self.changeNotificationNoteSettings = changeNotificationNoteSettings + self.changeNotificationMuteSettings = changeNotificationMuteSettings self.openPeerInfo = openPeerInfo } } @@ -61,8 +63,16 @@ private func preparedPeerInfoEntryTransition(account: Account, from fromEntries: } private struct PeerInfoEquatableState: Equatable { + let state: PeerInfoState? + static func ==(lhs: PeerInfoEquatableState, rhs: PeerInfoEquatableState) -> Bool { - + if let lhsState = lhs.state, let rhsState = rhs.state { + return lhsState.isEqual(to: rhsState) + } else if (lhs.state != nil) != (rhs.state != nil) { + return false + } else { + return true + } } } @@ -80,7 +90,18 @@ public final class PeerInfoController: ListController { private let changeSettingsDisposable = MetaDisposable() private var currentListStyle: PeerInfoListStyle = .plain - private var state = Promise(nil) + + private var state = PeerInfoEquatableState(state: nil) { + didSet { + self.statePromise.set(.single(self.state)) + } + } + private var statePromise = Promise(PeerInfoEquatableState(state: nil)) + + private var leftNavigationButtonItem: UIBarButtonItem? + private var leftNavigationButton: PeerInfoNavigationButton? + private var rightNavigationButtonItem: UIBarButtonItem? + private var rightNavigationButton: PeerInfoNavigationButton? public init(account: Account, peerId: PeerId) { self.account = account @@ -103,13 +124,17 @@ public final class PeerInfoController: ListController { override public func displayNodeDidLoad() { super.displayNodeDidLoad() - let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in + let interaction = PeerInfoControllerInteraction(updateState: { [weak self] f in + if let strongSelf = self { + strongSelf.state = PeerInfoEquatableState(state: f(strongSelf.state.state)) + } + }, openSharedMedia: { [weak self] in if let strongSelf = self { if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } } - }, changeNotificationNoteSettings: { [weak self] in + }, changeNotificationMuteSettings: { [weak self] in if let strongSelf = self { let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in @@ -167,9 +192,11 @@ public final class PeerInfoController: ListController { let previousEntries = Atomic<[PeerInfoSortableEntry]?>(value: nil) let account = self.account - let transition = account.viewTracker.peerView(self.peerId) - |> map { view -> (PeerInfoEntryTransition, PeerInfoListStyle, Bool, Bool) in - let entries = peerInfoEntries(view: view).map { PeerInfoSortableEntry(entry: $0) } + let transition = combineLatest(account.viewTracker.peerView(self.peerId), self.statePromise.get() + |> distinctUntilChanged) + |> map { view, state -> (PeerInfoEntryTransition, PeerInfoListStyle, Bool, Bool, PeerInfoNavigationButton?, PeerInfoNavigationButton?) in + let infoEntries = peerInfoEntries(view: view, state: state.state) + let entries = infoEntries.entries.map { PeerInfoSortableEntry(entry: $0) } assert(entries == entries.sorted()) let previous = previousEntries.swap(entries) let style: PeerInfoListStyle @@ -180,12 +207,49 @@ public final class PeerInfoController: ListController { } else { style = .plain } - return (preparedPeerInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), style, previous == nil, previous != nil) + return (preparedPeerInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), style, previous == nil, previous != nil, infoEntries.leftNavigationButton, infoEntries.rightNavigationButton) } |> deliverOnMainQueue - self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated) in - self?.enqueueTransition(transition, style: style, firstTime: firstTime, animated: animated) + self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated, leftButton, rightButton) in + if let strongSelf = self { + strongSelf.enqueueTransition(transition, style: style, firstTime: firstTime, animated: animated) + if let leftButton = leftButton { + if let leftNavigationButtonItem = strongSelf.leftNavigationButtonItem { + if leftNavigationButtonItem.title != leftButton.title { + strongSelf.leftNavigationButtonItem = UIBarButtonItem(title: leftButton.title, style: .plain, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + strongSelf.navigationItem.setLeftBarButton(strongSelf.leftNavigationButtonItem, animated: false) + } + strongSelf.leftNavigationButton = leftButton + } else { + strongSelf.leftNavigationButton = leftButton + strongSelf.leftNavigationButtonItem = UIBarButtonItem(title: leftButton.title, style: .plain, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + strongSelf.navigationItem.setLeftBarButton(strongSelf.leftNavigationButtonItem, animated: false) + } + } else if strongSelf.leftNavigationButtonItem != nil { + strongSelf.leftNavigationButtonItem = nil + strongSelf.leftNavigationButton = nil + strongSelf.navigationItem.setLeftBarButton(nil, animated: false) + } + + if let rightButton = rightButton { + if let rightNavigationButtonItem = strongSelf.rightNavigationButtonItem { + if rightNavigationButtonItem.title != rightButton.title { + strongSelf.rightNavigationButtonItem = UIBarButtonItem(title: rightButton.title, style: .plain, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed)) + strongSelf.navigationItem.setRightBarButton(strongSelf.rightNavigationButtonItem, animated: false) + } + strongSelf.rightNavigationButton = rightButton + } else { + strongSelf.rightNavigationButton = rightButton + strongSelf.rightNavigationButtonItem = UIBarButtonItem(title: rightButton.title, style: .plain, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed)) + strongSelf.navigationItem.setRightBarButton(strongSelf.rightNavigationButtonItem, animated: false) + } + } else if strongSelf.rightNavigationButtonItem != nil { + strongSelf.rightNavigationButtonItem = nil + strongSelf.rightNavigationButton = nil + strongSelf.navigationItem.setRightBarButton(nil, animated: false) + } + } })) } @@ -215,4 +279,16 @@ public final class PeerInfoController: ListController { } }) } + + @objc func leftNavigationButtonPressed() { + if let leftNavigationButton = self.leftNavigationButton { + self.state = PeerInfoEquatableState(state: leftNavigationButton.action(self.state.state)) + } + } + + @objc func rightNavigationButtonPressed() { + if let rightNavigationButton = self.rightNavigationButton { + self.state = PeerInfoEquatableState(state: rightNavigationButton.action(self.state.state)) + } + } } diff --git a/TelegramUI/PeerInfoDisclosureItem.swift b/TelegramUI/PeerInfoDisclosureItem.swift index 7496b8222b..653e00cec6 100644 --- a/TelegramUI/PeerInfoDisclosureItem.swift +++ b/TelegramUI/PeerInfoDisclosureItem.swift @@ -244,18 +244,10 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/TelegramUI/PeerInfoEntries.swift b/TelegramUI/PeerInfoEntries.swift index c2a1bc4749..ae4b233933 100644 --- a/TelegramUI/PeerInfoEntries.swift +++ b/TelegramUI/PeerInfoEntries.swift @@ -38,21 +38,24 @@ protocol PeerInfoEntry { func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem } -enum PeerInfoNavigationButton { - case none - case edit - case done +struct PeerInfoNavigationButton { + let title: String + let action: (PeerInfoState?) -> PeerInfoState? } protocol PeerInfoState { func isEqual(to: PeerInfoState) -> Bool - - var navigationButton: PeerInfoNavigationButton { get } } -func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] { +struct PeerInfoEntries { + let entries: [PeerInfoEntry] + let leftNavigationButton: PeerInfoNavigationButton? + let rightNavigationButton: PeerInfoNavigationButton? +} + +func peerInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { if let user = view.peers[view.peerId] as? TelegramUser { - return userInfoEntries(view: view) + return userInfoEntries(view: view, state: state) } else if let channel = view.peers[view.peerId] as? TelegramChannel { switch channel.info { case .broadcast: @@ -63,5 +66,5 @@ func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] { } else if let group = view.peers[view.peerId] as? TelegramGroup { return groupInfoEntries(view: view) } - return [] + return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil) } diff --git a/TelegramUI/PeerInfoPeerActionItem.swift b/TelegramUI/PeerInfoPeerActionItem.swift index e539a7e2f0..3065539bc1 100644 --- a/TelegramUI/PeerInfoPeerActionItem.swift +++ b/TelegramUI/PeerInfoPeerActionItem.swift @@ -206,18 +206,10 @@ class PeerInfoPeerActionItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/TelegramUI/PeerInfoPeerItem.swift b/TelegramUI/PeerInfoPeerItem.swift index 3c349e44b1..a78b837845 100644 --- a/TelegramUI/PeerInfoPeerItem.swift +++ b/TelegramUI/PeerInfoPeerItem.swift @@ -300,22 +300,10 @@ class PeerInfoPeerItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/TelegramUI/PeerInfoTextWithLabelItem.swift b/TelegramUI/PeerInfoTextWithLabelItem.swift index 099e1e8edf..2565b2fb2f 100644 --- a/TelegramUI/PeerInfoTextWithLabelItem.swift +++ b/TelegramUI/PeerInfoTextWithLabelItem.swift @@ -111,14 +111,10 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 74c94f47fd..5afc614fdc 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -176,7 +176,7 @@ public class PeerMediaCollectionController: ViewController { strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) }) } } - }) + }, sendSticker: { _ in }) self.controllerInteraction = controllerInteraction @@ -184,7 +184,9 @@ public class PeerMediaCollectionController: ViewController { }, forwardSelectedMessages: { - }, updateTextInputState: { _ in }) + }, updateTextInputState: { _ in + }, updateInputMode: { _ in + }) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 0265f41ea2..bbe0525da9 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -74,21 +74,21 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { insets.top += navigationBarHeight if let selectionState = self.mediaCollectionInterfaceState.selectionState { + let interfaceState = ChatPresentationInterfaceState().updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) + if let selectionPanel = self.selectionPanel { - selectionPanel.peer = self.mediaCollectionInterfaceState.peer selectionPanel.selectedMessageCount = selectionState.selectedIds.count - let panelSize = selectionPanel.measure(layout.size) - transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelSize.height), size: panelSize)) + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: transition, interfaceState: interfaceState) + transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) } else { let selectionPanel = ChatMessageSelectionInputPanelNode() - selectionPanel.peer = self.mediaCollectionInterfaceState.peer selectionPanel.selectedMessageCount = selectionState.selectedIds.count selectionPanel.backgroundColor = UIColor(0xfafafa) - let panelSize = selectionPanel.measure(layout.size) - selectionPanel.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom), size: panelSize) + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: interfaceState) self.selectionPanel = selectionPanel self.addSubnode(selectionPanel) - transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelSize.height), size: panelSize)) + selectionPanel.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) } } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 7e3ac92749..0f3e4064eb 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -11,25 +11,22 @@ func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMedia return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0)) } -private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Int), NoError> { - if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize), let smallestSize = smallestRepresentation.size, let largestSize = largestRepresentation.size { - let thumbnailResource = CloudFileMediaResource(location: smallestRepresentation.location, size: smallestSize) - let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize) +private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { + let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) - let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource) - - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Int), NoError> in - if maybeData.size >= fullSizeResource.size { + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((nil, loadedData, fullSizeResource.size)) + return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource) - let fetchedFullSize = account.postbox.mediaBox.fetchedResource(fullSizeResource) + let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource) + let fetchedFullSize = account.postbox.mediaBox.fetchedResource(largestRepresentation.resource) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in + let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -39,13 +36,13 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } } - let fullSizeData: Signal + let fullSizeData: Signal<(Data?, Bool), NoError> if autoFetchFullSize { - fullSizeData = Signal { subscriber in + fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = account.postbox.mediaBox.resourceData(fullSizeResource).start(next: { next in - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { @@ -54,16 +51,16 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } } } else { - fullSizeData = account.postbox.mediaBox.resourceData(fullSizeResource) - |> map { next -> Data? in - return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) + fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource) + |> map { next -> (Data?, Bool) in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in - return fullSizeData |> map { fullSizeData in - return (thumbnailData, fullSizeData, fullSizeResource.size) + return fullSizeData |> map { (fullSizeData, complete) in + return (thumbnailData, fullSizeData, complete) } } } @@ -75,18 +72,18 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } } -private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(Data?, (Data, String)?, Int), NoError> { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations), let smallestSize = smallestRepresentation.size { - let thumbnailResource = CloudFileMediaResource(location: smallestRepresentation.location, size: smallestSize) - let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size) +private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations), let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { + let thumbnailResource = smallestRepresentation.resource + let fullSizeResource = largestRepresentation.resource let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Int), NoError> in - if maybeData.size >= fullSizeResource.size { + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Bool), NoError> in + if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), fullSizeResource.size)) + return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), true)) } else { let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource) @@ -103,14 +100,14 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pro } - let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, complete: !progressive) |> map { next -> (Data, String)? in + let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, complete: !progressive) |> map { next -> ((Data, String)?, Bool) in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) - return data == nil ? nil : (data!, next.path) + return (data == nil ? nil : (data!, next.path), next.complete) } return thumbnail |> mapToSignal { thumbnailData in - return fullSizeDataAndPath |> map { dataAndPath in - return (thumbnailData, dataAndPath, fullSizeResource.size) + return fullSizeDataAndPath |> map { (dataAndPath, complete) in + return (thumbnailData, dataAndPath, complete) } } } @@ -372,7 +369,7 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { let signal = chatMessagePhotoDatas(account: account, photo: photo) - return signal |> map { (thumbnailData, fullSizeData, fullTotalSize) in + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -383,7 +380,7 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { - if fullSizeData.count >= fullTotalSize { + if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) @@ -392,7 +389,7 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr } } else { let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeData.count >= fullTotalSize) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -450,7 +447,7 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { let signal = chatMessagePhotoDatas(account: account, photo: photo, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) - return signal |> map { (thumbnailData, fullSizeData, fullTotalSize) in + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -461,7 +458,7 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { - if fullSizeData.count >= fullTotalSize { + if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) @@ -470,7 +467,7 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa } } else { let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeData.count >= fullTotalSize) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -526,34 +523,30 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa } func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size { - let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize) - return account.postbox.mediaBox.resourceStatus(fullSizeResource) + if let largestRepresentation = largestRepresentationForPhoto(photo) { + return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource) } else { return .never() } } func chatMessagePhotoInteractiveFetched(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size { - let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize) - return account.postbox.mediaBox.fetchedResource(fullSizeResource) + if let largestRepresentation = largestRepresentationForPhoto(photo) { + return account.postbox.mediaBox.fetchedResource(largestRepresentation.resource) } else { return .never() } } func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) { - if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size { - let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize) - return account.postbox.mediaBox.cancelInteractiveResourceFetch(fullSizeResource) + if let largestRepresentation = largestRepresentationForPhoto(photo) { + return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) } } func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> Signal { if let closestRepresentation = photo.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) { - let resource = CloudFileMediaResource(location: closestRepresentation.location, size: closestRepresentation.size ?? 0) - let resourceData = account.postbox.mediaBox.resourceData(resource) |> map { next in + let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } @@ -566,7 +559,7 @@ func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> }, completed: { subscriber.putCompletion() })) - disposable.add(account.postbox.mediaBox.fetchedResource(resource).start()) + disposable.add(account.postbox.mediaBox.fetchedResource(closestRepresentation.resource).start()) return disposable } } else { @@ -618,7 +611,7 @@ func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage) -> Sig func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { let signal = chatMessageFileDatas(account: account, file: video) - return signal |> map { (thumbnailData, fullSizeDataAndPath, fullTotalSize) in + return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -632,7 +625,7 @@ func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(Tra var fullSizeImage: CGImage? if let fullSizeDataAndPath = fullSizeDataAndPath { - if fullSizeDataAndPath.0.count >= fullTotalSize { + if fullSizeComplete { if video.mimeType.hasPrefix("video/") { let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" @@ -714,7 +707,7 @@ func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(Tra func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { let signal = chatMessageFileDatas(account: account, file: file, progressive: progressive) - return signal |> map { (thumbnailData, fullSizeDataAndPath, fullTotalSize) in + return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -725,7 +718,7 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive var fullSizeImage: CGImage? if let fullSizeDataAndPath = fullSizeDataAndPath { - if fullSizeDataAndPath.0.count >= fullTotalSize { + if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) @@ -734,7 +727,7 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive } } else if progressive { let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeDataAndPath.0 as CFData, fullSizeDataAndPath.0.count >= fullTotalSize) + CGImageSourceUpdateData(imageSource, fullSizeDataAndPath.0 as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -790,16 +783,13 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive } func chatMessageFileStatus(account: Account, file: TelegramMediaFile) -> Signal { - let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size) - return account.postbox.mediaBox.resourceStatus(fullSizeResource) + return account.postbox.mediaBox.resourceStatus(file.resource) } func chatMessageFileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal { - let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size) - return account.postbox.mediaBox.fetchedResource(fullSizeResource) + return account.postbox.mediaBox.fetchedResource(file.resource) } func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) { - let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size) - return account.postbox.mediaBox.cancelInteractiveResourceFetch(fullSizeResource) + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 31b71bdca8..5a5e942bb4 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -5,42 +5,71 @@ import Display import TelegramUIPrivateModule import TelegramCore -private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile) -> Signal<(Data?, Data?, Int), NoError> { - let fullSizeResource = fileResource(file) - let maybeFetched = account.postbox.mediaBox.resourceData(fullSizeResource, complete: true) +private func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { + if let (colorData, alphaData) = data.withUnsafeBytes({ (bytes: UnsafePointer) -> (Data, Data)? in + var colorSize: Int32 = 0 + memcpy(&colorSize, bytes, 4) + if colorSize < 0 || Int(colorSize) > data.count - 8 { + return nil + } + var alphaSize: Int32 = 0 + memcpy(&alphaSize, bytes.advanced(by: 4 + Int(colorSize)), 4) + if alphaSize < 0 || Int(alphaSize) > data.count - Int(colorSize) - 8 { + return nil + } + //let colorData = Data(bytesNoCopy: UnsafeMutablePointer(mutating: bytes).advanced(by: 4), count: Int(colorSize), deallocator: .none) + //let alphaData = Data(bytesNoCopy: UnsafeMutablePointer(mutating: bytes).advanced(by: 4 + Int(colorSize) + 4), count: Int(alphaSize), deallocator: .none) + let colorData = data.subdata(in: 4 ..< (4 + Int(colorSize))) + let alphaData = data.subdata(in: (4 + Int(colorSize) + 4) ..< (4 + Int(colorSize) + 4 + Int(alphaSize))) + return (colorData, alphaData) + }) { + if let colorImage = UIImage(data: colorData), let alphaImage = UIImage(data: alphaData) { + return (colorImage, alphaImage) + + /*return generateImage(CGSize(width: colorImage.size.width * colorImage.scale, height: colorImage.size.height * colorImage.scale), contextGenerator: { size, context in + colorImage.draw(in: CGRect(origin: CGPoint(), size: size)) + }, scale: 1.0)*/ + } + } + return nil +} + +private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, small: Bool) -> Signal<(Data?, Data?, Bool), NoError> { + //let maybeFetched = account.postbox.mediaBox.resourceData(file.resource, complete: true) + let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil)) return maybeFetched |> take(1) |> mapToSignal { maybeData in - if maybeData.size >= fullSizeResource.size { + if maybeData.size >= file.size { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((nil, loadedData, fullSizeResource.size)) + return .single((nil, loadedData, true)) } else { - let fullSizeData = account.postbox.mediaBox.resourceData(fullSizeResource, complete: true) |> map { next in - return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + //let fullSizeData = account.postbox.mediaBox.resourceData(file.resource, complete: true) + + let fullSizeData = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil)) |> map { next in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) } - return fullSizeData |> map { data -> (Data?, Data?, Int) in - return (nil, data, fullSizeResource.size) + return fullSizeData |> map { (data, complete) -> (Data?, Data?, Bool) in + return (nil, data, complete) } } } } -func chatMessageSticker(account: Account, file: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { - let signal = chatMessageStickerDatas(account: account, file: file) +func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> { + let signal = chatMessageStickerDatas(account: account, file: file, small: small) - return signal |> map { (thumbnailData, fullSizeData, fullTotalSize) in + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in assertNotOnMainThread() + let context = DrawingContext(size: arguments.drawingSize, clear: true) - var fullSizeImage: UIImage? - if let fullSizeData = fullSizeData { - if fullSizeData.count >= fullTotalSize { - if let image = UIImage.convert(fromWebP: fullSizeData) { - fullSizeImage = image - } - } else { + var fullSizeImage: (UIImage, UIImage)? + if let fullSizeData = fullSizeData, fullSizeComplete { + if let image = imageFromAJpeg(data: fullSizeData) { + fullSizeImage = image } } @@ -67,10 +96,13 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile) -> Signal<(Tr c.draw(blurredThumbnailImage.cgImage!, in: arguments.drawingRect) } - if let fullSizeImage = fullSizeImage, let cgImage = fullSizeImage.cgImage { + if let fullSizeImage = fullSizeImage, let cgImage = fullSizeImage.0.cgImage, let cgImageAlpha = fullSizeImage.1.cgImage { c.setBlendMode(.normal) c.interpolationQuality = .medium - c.draw(cgImage, in: arguments.drawingRect) + + let mask = CGImage(maskWidth: cgImageAlpha.width, height: cgImageAlpha.height, bitsPerComponent: cgImageAlpha.bitsPerComponent, bitsPerPixel: cgImageAlpha.bitsPerPixel, bytesPerRow: cgImageAlpha.bytesPerRow, provider: cgImageAlpha.dataProvider!, decode: nil, shouldInterpolate: true) + + c.draw(cgImage.masking(mask!)!, in: arguments.drawingRect) } } diff --git a/TelegramUI/UserInfoEntries.swift b/TelegramUI/UserInfoEntries.swift index e8fd1a7de5..3a4343f7fa 100644 --- a/TelegramUI/UserInfoEntries.swift +++ b/TelegramUI/UserInfoEntries.swift @@ -25,8 +25,13 @@ private enum UserInfoSection: UInt32, PeerInfoSection { } } +enum DestructiveUserInfoAction { + case block + case removeContact +} + enum UserInfoEntry: PeerInfoEntry { - case info(peer: Peer?, cachedData: CachedPeerData?) + case info(peer: Peer?, cachedData: CachedPeerData?, editingState: PeerInfoAvatarAndNameItemEditingState?) case about(text: String) case phoneNumber(index: Int, value: PhoneNumberWithLabel) case userName(value: String) @@ -35,7 +40,8 @@ enum UserInfoEntry: PeerInfoEntry { case startSecretChat case sharedMedia case notifications(settings: PeerNotificationSettings?) - case block + case notificationSound(settings: PeerNotificationSettings?) + case block(action: DestructiveUserInfoAction) var section: PeerInfoSection { switch self { @@ -43,7 +49,7 @@ enum UserInfoEntry: PeerInfoEntry { return UserInfoSection.info case .sendMessage, .shareContact, .startSecretChat: return UserInfoSection.actions - case .sharedMedia, .notifications: + case .sharedMedia, .notifications, .notificationSound: return UserInfoSection.sharedMediaAndNotifications case .block: return UserInfoSection.block @@ -60,9 +66,9 @@ enum UserInfoEntry: PeerInfoEntry { } switch self { - case let .info(lhsPeer, lhsCachedData): + case let .info(lhsPeer, lhsCachedData, lhsEditingState): switch entry { - case let .info(rhsPeer, rhsCachedData): + case let .info(rhsPeer, rhsCachedData, rhsEditingState): if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -77,6 +83,9 @@ enum UserInfoEntry: PeerInfoEntry { } else if (lhsCachedData != nil) != (rhsCachedData != nil) { return false } + if lhsEditingState != rhsEditingState { + return false + } return true default: return false @@ -142,9 +151,21 @@ enum UserInfoEntry: PeerInfoEntry { default: return false } - case .block: + case let .notificationSound(lhsSettings): switch entry { - case .block: + case let .notificationSound(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case let .block(action): + switch entry { + case .block(action): return true default: return false @@ -172,8 +193,10 @@ enum UserInfoEntry: PeerInfoEntry { return 1004 case .notifications: return 1005 - case .block: + case .notificationSound: return 1006 + case .block: + return 1007 } } @@ -187,8 +210,8 @@ enum UserInfoEntry: PeerInfoEntry { func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { switch self { - case let .info(peer, cachedData): - return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain) + case let .info(peer, cachedData, editingState): + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, editingState: editingState, sectionId: self.section.rawValue, style: .plain) case let .about(text): return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue) case let .phoneNumber(_, value): @@ -219,65 +242,141 @@ enum UserInfoEntry: PeerInfoEntry { label = "Enabled" } return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: { - interaction.changeNotificationNoteSettings() + interaction.changeNotificationMuteSettings() }) - case .block: - return PeerInfoActionItem(title: "Block User", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + case let .notificationSound(settings): + let label: String + label = "Default" + return PeerInfoDisclosureItem(title: "Sound", label: label, sectionId: self.section.rawValue, style: .plain, action: { + }) + case let .block(action): + let title: String + switch action { + case .block: + title = "Block User" + case .removeContact: + title = "Remove Contact" + } + return PeerInfoActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { }) } } } -final class UserInfoEditingState { +final class UserInfoEditingState: Equatable { + let infoState = PeerInfoAvatarAndNameItemEditingState() + static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool { + return true + } } -final class UserInfoState: PeerInfoState { +private final class UserInfoState: PeerInfoState { fileprivate let editingState: UserInfoEditingState? - var navigationButton: PeerInfoNavigationButton { - return self.editingState == nil ? .edit : .done - } - - init() { - self.editingState = nil + init(editingState: UserInfoEditingState?) { + self.editingState = editingState } func isEqual(to: PeerInfoState) -> Bool { if let to = to as? UserInfoState { - return true + return self.editingState == to.editingState } else { return false } } + + func updateEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState { + return UserInfoState(editingState: editingState) + } } -func userInfoEntries(view: PeerView, state: PeerInfoState?) -> [PeerInfoEntry] { +func userInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { var entries: [PeerInfoEntry] = [] - entries.append(UserInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) + + var infoEditingState: PeerInfoAvatarAndNameItemEditingState? + + var isEditing = false + if let state = state as? UserInfoState, let editingState = state.editingState { + isEditing = true + + if view.peerIsContact { + infoEditingState = editingState.infoState + } + } + + entries.append(UserInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData, editingState: infoEditingState)) if let cachedUserData = view.cachedData as? CachedUserData { if let about = cachedUserData.about, !about.isEmpty { entries.append(UserInfoEntry.about(text: about)) } } + + var editable = true + if let user = view.peers[view.peerId] as? TelegramUser { if let phoneNumber = user.phone, !phoneNumber.isEmpty { entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) } - if let username = user.username, !username.isEmpty { - entries.append(UserInfoEntry.userName(value: username)) - } - if let state = state as? UserInfoState, let editingState = state.editingState { - - } else { + + if !isEditing { + if let username = user.username, !username.isEmpty { + entries.append(UserInfoEntry.userName(value: username)) + } entries.append(UserInfoEntry.sendMessage) - entries.append(UserInfoEntry.shareContact) + if view.peerIsContact { + entries.append(UserInfoEntry.shareContact) + } entries.append(UserInfoEntry.startSecretChat) + entries.append(UserInfoEntry.sharedMedia) } - entries.append(UserInfoEntry.sharedMedia) entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) - entries.append(UserInfoEntry.block) + + if isEditing { + entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings)) + if view.peerIsContact { + entries.append(UserInfoEntry.block(action: .removeContact)) + } + } else { + entries.append(UserInfoEntry.block(action: .block)) + } } - return entries + + var leftNavigationButton: PeerInfoNavigationButton? + var rightNavigationButton: PeerInfoNavigationButton? + if editable { + if let state = state as? UserInfoState, let _ = state.editingState { + leftNavigationButton = PeerInfoNavigationButton(title: "Cancel", action: { state in + if state == nil { + return UserInfoState(editingState: nil) + } else if let state = state as? UserInfoState { + return state.updateEditingState(nil) + } else { + return state + } + }) + rightNavigationButton = PeerInfoNavigationButton(title: "Done", action: { state in + if state == nil { + return UserInfoState(editingState: nil) + } else if let state = state as? UserInfoState { + return state.updateEditingState(nil) + } else { + return state + } + }) + } else { + rightNavigationButton = PeerInfoNavigationButton(title: "Edit", action: { state in + if state == nil { + return UserInfoState(editingState: UserInfoEditingState()) + } else if let state = state as? UserInfoState { + return state.updateEditingState(UserInfoEditingState()) + } else { + return state + } + }) + } + } + + return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) }