mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
no message
This commit is contained in:
12
Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ic_bot.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/ic_bot.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Input/Text/AccessoryIconInputButtons.imageset/ic_bot.pdf
vendored
Normal file
Binary file not shown.
12
Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ic_keyboard.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/ic_keyboard.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Input/Text/AccessoryIconKeyboard.imageset/ic_keyboard.pdf
vendored
Normal file
Binary file not shown.
12
Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Input/Text/AccessoryIconStickers.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ic_sticker.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "IconAttachment.pdf"
|
"filename" : "ic_attach.pdf"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
BIN
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf
vendored
Normal file
Binary file not shown.
12
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ic_voice.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf
vendored
Normal file
Binary file not shown.
12
Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json
vendored
Normal file
12
Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "Send.pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf
vendored
Normal file
BIN
Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf
vendored
Normal file
Binary file not shown.
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "builtin-wallpaper-0.jpg",
|
"filename" : "Dogs BG.jpg",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
BIN
Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg
vendored
Normal file
BIN
Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB |
@@ -11,6 +11,10 @@
|
|||||||
D00370301DA43077004308D3 /* PeerInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003702F1DA43077004308D3 /* PeerInfoItem.swift */; };
|
D00370301DA43077004308D3 /* PeerInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003702F1DA43077004308D3 /* PeerInfoItem.swift */; };
|
||||||
D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */; };
|
D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */; };
|
||||||
D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.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 */; };
|
D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; };
|
||||||
D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; };
|
D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; };
|
||||||
D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0761D9190EF000889C2 /* GridMessageItem.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 */; };
|
D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; };
|
||||||
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; };
|
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; };
|
||||||
D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.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 */; };
|
D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */; };
|
||||||
D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA41D95783C005BCD27 /* ListMessageNode.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 */; };
|
D08D452E1D5E340300A7428A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D45291D5E340300A7428A /* AsyncDisplayKit.framework */; };
|
||||||
D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; };
|
D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; };
|
||||||
D08D45301D5E340300A7428A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452B1D5E340300A7428A /* Postbox.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 */; };
|
D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */; };
|
||||||
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */; };
|
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */; };
|
||||||
D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.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 */; };
|
D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; };
|
||||||
D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */; };
|
D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */; };
|
||||||
D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC21D6B89DA0046BCD6 /* TextNode.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 = "<group>"; };
|
D003702F1DA43077004308D3 /* PeerInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoItem.swift; sourceTree = "<group>"; };
|
||||||
D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoTextWithLabelItem.swift; sourceTree = "<group>"; };
|
D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoTextWithLabelItem.swift; sourceTree = "<group>"; };
|
||||||
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = "<group>"; };
|
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = "<group>"; };
|
||||||
|
D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputNode.swift; sourceTree = "<group>"; };
|
||||||
|
D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputNode.swift; sourceTree = "<group>"; };
|
||||||
|
D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputNodes.swift; sourceTree = "<group>"; };
|
||||||
|
D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerPackItem.swift; sourceTree = "<group>"; };
|
||||||
D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = "<group>"; };
|
D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = "<group>"; };
|
D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = "<group>"; };
|
||||||
D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = "<group>"; };
|
D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = "<group>"; };
|
||||||
@@ -230,8 +242,13 @@
|
|||||||
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||||
D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = "<group>"; };
|
D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = "<group>"; };
|
||||||
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
|
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||||
|
D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = "<group>"; };
|
||||||
|
D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = "<group>"; };
|
||||||
D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = "<group>"; };
|
D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = "<group>"; };
|
||||||
D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = "<group>"; };
|
D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = "<group>"; };
|
||||||
|
D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = "<group>"; };
|
||||||
|
D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = "<group>"; };
|
||||||
|
D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
@@ -307,7 +324,6 @@
|
|||||||
D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlaybackData.swift; sourceTree = "<group>"; };
|
D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlaybackData.swift; sourceTree = "<group>"; };
|
||||||
D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaVideoFrameDecoder.swift; sourceTree = "<group>"; };
|
D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaVideoFrameDecoder.swift; sourceTree = "<group>"; };
|
||||||
D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackFrameDecoder.swift; sourceTree = "<group>"; };
|
D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackFrameDecoder.swift; sourceTree = "<group>"; };
|
||||||
D0F69D901D6B87EC0046BCD6 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
|
||||||
D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = TelegramUI.xcconfig; path = TelegramUI/Config/TelegramUI.xcconfig; sourceTree = "<group>"; };
|
D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = TelegramUI.xcconfig; path = TelegramUI/Config/TelegramUI.xcconfig; sourceTree = "<group>"; };
|
||||||
D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionHeaderNode.swift; sourceTree = "<group>"; };
|
D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionHeaderNode.swift; sourceTree = "<group>"; };
|
||||||
D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextNode.swift; sourceTree = "<group>"; };
|
D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextNode.swift; sourceTree = "<group>"; };
|
||||||
@@ -470,6 +486,27 @@
|
|||||||
name = Components;
|
name = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D021E0CC1DB4132E00C6B04F /* Input Nodes */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */,
|
||||||
|
D021E0E31DB55CDB00C6B04F /* Media */,
|
||||||
|
);
|
||||||
|
name = "Input Nodes";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D021E0E31DB55CDB00C6B04F /* Media */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */,
|
||||||
|
D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */,
|
||||||
|
D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */,
|
||||||
|
D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */,
|
||||||
|
D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */,
|
||||||
|
);
|
||||||
|
name = Media;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D02BE0751D9190CD000889C2 /* Grid Items */ = {
|
D02BE0751D9190CD000889C2 /* Grid Items */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -492,6 +529,7 @@
|
|||||||
D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */,
|
D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */,
|
||||||
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */,
|
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */,
|
||||||
D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */,
|
D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */,
|
||||||
|
D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */,
|
||||||
);
|
);
|
||||||
name = "Interface State";
|
name = "Interface State";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -686,7 +724,6 @@
|
|||||||
children = (
|
children = (
|
||||||
D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */,
|
D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */,
|
||||||
D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */,
|
D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */,
|
||||||
D0F69D901D6B87EC0046BCD6 /* Cache.swift */,
|
|
||||||
D0F69DBC1D6B886C0046BCD6 /* Player */,
|
D0F69DBC1D6B886C0046BCD6 /* Player */,
|
||||||
D0F69E9D1D6B8E240046BCD6 /* Resources */,
|
D0F69E9D1D6B8E240046BCD6 /* Resources */,
|
||||||
);
|
);
|
||||||
@@ -867,9 +904,10 @@
|
|||||||
D0F69E181D6B8AD10046BCD6 /* Items */,
|
D0F69E181D6B8AD10046BCD6 /* Items */,
|
||||||
D03ADB461D703250005A521C /* Interface State */,
|
D03ADB461D703250005A521C /* Interface State */,
|
||||||
D03ADB491D704427005A521C /* Accessory Panels */,
|
D03ADB491D704427005A521C /* Accessory Panels */,
|
||||||
|
D021E0CC1DB4132E00C6B04F /* Input Nodes */,
|
||||||
D0DF0C961D81FD87008AEB01 /* Input Context Panels */,
|
D0DF0C961D81FD87008AEB01 /* Input Context Panels */,
|
||||||
D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */,
|
|
||||||
D0BA6F811D784C3A0034826E /* Input Panels */,
|
D0BA6F811D784C3A0034826E /* Input Panels */,
|
||||||
|
D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */,
|
||||||
D0F69E441D6B8B850046BCD6 /* History Navigation */,
|
D0F69E441D6B8B850046BCD6 /* History Navigation */,
|
||||||
D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */,
|
D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */,
|
||||||
);
|
);
|
||||||
@@ -1033,6 +1071,8 @@
|
|||||||
D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */,
|
D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */,
|
||||||
D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */,
|
D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */,
|
||||||
D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */,
|
D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */,
|
||||||
|
D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */,
|
||||||
|
D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */,
|
||||||
);
|
);
|
||||||
name = Resources;
|
name = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1205,10 +1245,13 @@
|
|||||||
D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */,
|
D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */,
|
||||||
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */,
|
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */,
|
||||||
D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */,
|
D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */,
|
||||||
|
D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */,
|
||||||
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */,
|
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */,
|
||||||
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
|
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
|
||||||
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
|
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
|
||||||
|
D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */,
|
||||||
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */,
|
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */,
|
||||||
|
D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */,
|
||||||
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */,
|
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */,
|
||||||
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */,
|
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */,
|
||||||
D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */,
|
D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */,
|
||||||
@@ -1229,8 +1272,10 @@
|
|||||||
D0DE77291D932923002B8809 /* GridMessageSelectionNode.swift in Sources */,
|
D0DE77291D932923002B8809 /* GridMessageSelectionNode.swift in Sources */,
|
||||||
D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */,
|
D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */,
|
||||||
D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */,
|
D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */,
|
||||||
|
D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */,
|
||||||
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */,
|
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */,
|
||||||
D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */,
|
D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */,
|
||||||
|
D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */,
|
||||||
D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */,
|
D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */,
|
||||||
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */,
|
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */,
|
||||||
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */,
|
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */,
|
||||||
@@ -1249,6 +1294,7 @@
|
|||||||
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */,
|
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */,
|
||||||
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
|
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
|
||||||
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
|
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
|
||||||
|
D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */,
|
||||||
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */,
|
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */,
|
||||||
D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */,
|
D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */,
|
||||||
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
|
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
|
||||||
@@ -1272,13 +1318,16 @@
|
|||||||
D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */,
|
D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */,
|
||||||
D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */,
|
D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */,
|
||||||
D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */,
|
D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */,
|
||||||
|
D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */,
|
||||||
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */,
|
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */,
|
||||||
D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */,
|
D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */,
|
||||||
|
D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */,
|
||||||
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */,
|
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */,
|
||||||
D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */,
|
D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */,
|
||||||
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */,
|
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */,
|
||||||
D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */,
|
D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */,
|
||||||
D0DE77321D940295002B8809 /* ListMessageFileItemNode.swift in Sources */,
|
D0DE77321D940295002B8809 /* ListMessageFileItemNode.swift in Sources */,
|
||||||
|
D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */,
|
||||||
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */,
|
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */,
|
||||||
D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */,
|
D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */,
|
||||||
D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */,
|
D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */,
|
||||||
@@ -1296,7 +1345,6 @@
|
|||||||
D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */,
|
D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */,
|
||||||
D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */,
|
D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */,
|
||||||
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */,
|
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */,
|
||||||
D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */,
|
|
||||||
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */,
|
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */,
|
||||||
D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */,
|
D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */,
|
||||||
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
|
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ private let testBackground = generateStretchableFilledCircleImage(radius: 8.0, c
|
|||||||
|
|
||||||
final class ActionSheetRollImageItem: ListViewItem {
|
final class ActionSheetRollImageItem: ListViewItem {
|
||||||
let asset: PHAsset
|
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
|
self.asset = asset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +33,10 @@ final class ActionSheetRollImageItem: ListViewItem {
|
|||||||
completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {
|
completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selected(listView: ListView) {
|
||||||
|
self.selectedItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ActionSheetRollImageItemNode: ListViewItemNode {
|
private final class ActionSheetRollImageItemNode: ListViewItemNode {
|
||||||
|
|||||||
@@ -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<Data, NoError> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
TelegramUI/CachedResourceRepresentations.swift
Normal file
27
TelegramUI/CachedResourceRepresentations.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,7 +153,7 @@ enum ChannelInfoEntry: PeerInfoEntry {
|
|||||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||||
switch self {
|
switch self {
|
||||||
case let .info(peer, cachedData):
|
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):
|
case let .about(text):
|
||||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
||||||
case let .userName(value):
|
case let .userName(value):
|
||||||
@@ -173,7 +173,7 @@ enum ChannelInfoEntry: PeerInfoEntry {
|
|||||||
label = "Enabled"
|
label = "Enabled"
|
||||||
}
|
}
|
||||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
||||||
interaction.changeNotificationNoteSettings()
|
interaction.changeNotificationMuteSettings()
|
||||||
})
|
})
|
||||||
case .report:
|
case .report:
|
||||||
return PeerInfoActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
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] = []
|
var entries: [PeerInfoEntry] = []
|
||||||
entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
||||||
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
||||||
@@ -206,5 +206,5 @@ func channelBroadcastInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
|||||||
entries.append(ChannelInfoEntry.leave)
|
entries.append(ChannelInfoEntry.leave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries
|
return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,23 +49,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
|||||||
|
|
||||||
private let actionDisposable = MetaDisposable()
|
private let actionDisposable = MetaDisposable()
|
||||||
|
|
||||||
override var peer: Peer? {
|
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.button = UIButton()
|
self.button = UIButton()
|
||||||
@@ -84,23 +68,6 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
|||||||
self.actionDisposable.dispose()
|
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? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.bounds.contains(point) {
|
if self.bounds.contains(point) {
|
||||||
return self.button
|
return self.button
|
||||||
@@ -110,7 +77,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func buttonPressed() {
|
@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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +102,34 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
|||||||
break
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class ChatController: ViewController {
|
|||||||
private var didSetPeerReady = false
|
private var didSetPeerReady = false
|
||||||
private let peerView = Promise<PeerView>()
|
private let peerView = Promise<PeerView>()
|
||||||
|
|
||||||
private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil, inputContext: nil)
|
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
||||||
private let chatInterfaceStatePromise = Promise<ChatInterfaceState>()
|
private let chatInterfaceStatePromise = Promise<ChatInterfaceState>()
|
||||||
|
|
||||||
private var chatTitleView: ChatTitleView?
|
private var chatTitleView: ChatTitleView?
|
||||||
@@ -154,13 +154,17 @@ public class ChatController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, clickThroughMessage: { [weak self] in
|
}, clickThroughMessage: { [weak self] in
|
||||||
self?.view.endEditing(true)
|
self?.chatDisplayNode.dismissInput()
|
||||||
}, toggleMessageSelection: { [weak self] id in
|
}, toggleMessageSelection: { [weak self] id in
|
||||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) {
|
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
|
self.controllerInteraction = controllerInteraction
|
||||||
@@ -173,7 +177,7 @@ public class ChatController: ViewController {
|
|||||||
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||||||
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem)
|
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))
|
self.peerView.set(account.viewTracker.peerView(peerId))
|
||||||
|
|
||||||
@@ -184,7 +188,7 @@ public class ChatController: ViewController {
|
|||||||
strongSelf.chatTitleView?.peerView = peerView
|
strongSelf.chatTitleView?.peerView = peerView
|
||||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
(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 {
|
if !strongSelf.didSetPeerReady {
|
||||||
strongSelf.didSetPeerReady = true
|
strongSelf.didSetPeerReady = true
|
||||||
strongSelf._peerReady.set(.single(true))
|
strongSelf._peerReady.set(.single(true))
|
||||||
@@ -288,12 +292,23 @@ public class ChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in
|
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
|
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let controller = ChatMediaActionSheetController()
|
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
|
controller.location = { [weak strongSelf] in
|
||||||
if let strongSelf = strongSelf {
|
if let strongSelf = strongSelf {
|
||||||
let mapInputController = MapInputController()
|
let mapInputController = MapInputController()
|
||||||
@@ -317,14 +332,14 @@ public class ChatController: ViewController {
|
|||||||
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in
|
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in
|
||||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
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()
|
strongSelf.chatDisplayNode.ensureInputViewFocused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, beginMessageSelection: { [weak self] messageId in
|
}, beginMessageSelection: { [weak self] messageId in
|
||||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
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
|
}, deleteSelectedMessages: { [weak self] in
|
||||||
@@ -334,7 +349,7 @@ public class ChatController: ViewController {
|
|||||||
modifier.deleteMessages(Array(messageIds))
|
modifier.deleteMessages(Array(messageIds))
|
||||||
}).start()
|
}).start()
|
||||||
}
|
}
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||||
}
|
}
|
||||||
}, forwardSelectedMessages: { [weak self] in
|
}, forwardSelectedMessages: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@@ -343,7 +358,11 @@ public class ChatController: ViewController {
|
|||||||
}
|
}
|
||||||
}, updateTextInputState: { [weak self] textInputState in
|
}, updateTextInputState: { [weak self] textInputState in
|
||||||
if let strongSelf = self {
|
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.preloadPages = true
|
||||||
self.chatDisplayNode.historyNode.canReadHistory.set(.single(true))
|
self.chatDisplayNode.historyNode.canReadHistory.set(.single(true))
|
||||||
|
|
||||||
|
self.chatDisplayNode.loadInputPanels()
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
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 temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState)
|
||||||
let inputContext = inputContextForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account)
|
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 {
|
if self.isNodeLoaded {
|
||||||
self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated)
|
self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive)
|
||||||
}
|
}
|
||||||
self.presentationInterfaceState = updatedChatPresentationInterfaceState
|
self.presentationInterfaceState = updatedChatPresentationInterfaceState
|
||||||
self.chatInterfaceStatePromise.set(.single(updatedChatPresentationInterfaceState.interfaceState))
|
self.chatInterfaceStatePromise.set(.single(updatedChatPresentationInterfaceState.interfaceState))
|
||||||
@@ -429,7 +451,7 @@ public class ChatController: ViewController {
|
|||||||
private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
|
private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
|
||||||
switch action {
|
switch action {
|
||||||
case .cancelMessageSelection:
|
case .cancelMessageSelection:
|
||||||
self.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||||
case .clearHistory:
|
case .clearHistory:
|
||||||
let actionSheet = ActionSheetController()
|
let actionSheet = ActionSheetController()
|
||||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
import Postbox
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
public enum ChatControllerInteractionNavigateToPeer {
|
public enum ChatControllerInteractionNavigateToPeer {
|
||||||
case chat
|
case chat
|
||||||
@@ -16,13 +17,15 @@ public final class ChatControllerInteraction {
|
|||||||
var hiddenMedia: [MessageId: [Media]] = [:]
|
var hiddenMedia: [MessageId: [Media]] = [:]
|
||||||
var selectionState: ChatInterfaceSelectionState?
|
var selectionState: ChatInterfaceSelectionState?
|
||||||
let toggleMessageSelection: (MessageId) -> Void
|
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.openMessage = openMessage
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
self.openMessageContextMenu = openMessageContextMenu
|
self.openMessageContextMenu = openMessageContextMenu
|
||||||
self.navigateToMessage = navigateToMessage
|
self.navigateToMessage = navigateToMessage
|
||||||
self.clickThroughMessage = clickThroughMessage
|
self.clickThroughMessage = clickThroughMessage
|
||||||
self.toggleMessageSelection = toggleMessageSelection
|
self.toggleMessageSelection = toggleMessageSelection
|
||||||
|
self.sendSticker = sendSticker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: Ch
|
|||||||
class ChatControllerNode: ASDisplayNode {
|
class ChatControllerNode: ASDisplayNode {
|
||||||
let account: Account
|
let account: Account
|
||||||
let peerId: PeerId
|
let peerId: PeerId
|
||||||
|
let controllerInteraction: ChatControllerInteraction
|
||||||
|
|
||||||
let backgroundNode: ASDisplayNode
|
let backgroundNode: ASDisplayNode
|
||||||
let historyNode: ChatHistoryListNode
|
let historyNode: ChatHistoryListNode
|
||||||
@@ -26,7 +27,10 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
private var accessoryPanelNode: AccessoryPanelNode?
|
private var accessoryPanelNode: AccessoryPanelNode?
|
||||||
private var inputContextPanelNode: ChatInputContextPanelNode?
|
private var inputContextPanelNode: ChatInputContextPanelNode?
|
||||||
|
|
||||||
|
private var inputNode: ChatInputNode?
|
||||||
|
|
||||||
private var textInputPanelNode: ChatTextInputPanelNode?
|
private var textInputPanelNode: ChatTextInputPanelNode?
|
||||||
|
private var inputMediaNode: ChatMediaInputNode?
|
||||||
|
|
||||||
let navigateToLatestButton: ChatHistoryNavigationButtonNode
|
let navigateToLatestButton: ChatHistoryNavigationButtonNode
|
||||||
|
|
||||||
@@ -41,9 +45,15 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
|
|
||||||
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
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) {
|
init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
|
self.controllerInteraction = controllerInteraction
|
||||||
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.isLayerBacked = true
|
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.historyNode = ChatHistoryListNode(account: account, peerId: peerId, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction)
|
||||||
|
|
||||||
self.inputPanelBackgroundNode = ASDisplayNode()
|
self.inputPanelBackgroundNode = ASDisplayNode()
|
||||||
self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa)
|
self.inputPanelBackgroundNode.backgroundColor = UIColor(0xF5F6F8)
|
||||||
self.inputPanelBackgroundNode.isLayerBacked = true
|
self.inputPanelBackgroundNode.isLayerBacked = true
|
||||||
|
|
||||||
self.inputPanelBackgroundSeparatorNode = ASDisplayNode()
|
self.inputPanelBackgroundSeparatorNode = ASDisplayNode()
|
||||||
self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xcdccd3)
|
self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xC9CDD1)
|
||||||
self.inputPanelBackgroundSeparatorNode.isLayerBacked = true
|
self.inputPanelBackgroundSeparatorNode.isLayerBacked = true
|
||||||
|
|
||||||
self.navigateToLatestButton = ChatHistoryNavigationButtonNode()
|
self.navigateToLatestButton = ChatHistoryNavigationButtonNode()
|
||||||
@@ -113,14 +123,58 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) {
|
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
|
insets.top += navigationBarHeight
|
||||||
|
|
||||||
var duration: Double = 0.0
|
var duration: Double = 0.0
|
||||||
var curve: UInt = 0
|
var curve: UInt = 0
|
||||||
|
var animated = true
|
||||||
switch transition {
|
switch transition {
|
||||||
case .immediate:
|
case .immediate:
|
||||||
break
|
animated = false
|
||||||
case let .animated(animationDuration, animationCurve):
|
case let .animated(animationDuration, animationCurve):
|
||||||
duration = animationDuration
|
duration = animationDuration
|
||||||
switch animationCurve {
|
switch animationCurve {
|
||||||
@@ -150,16 +204,19 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
var inputPanelSize: CGSize?
|
var inputPanelSize: CGSize?
|
||||||
var immediatelyLayoutInputPanelAndAnimateAppearance = false
|
var immediatelyLayoutInputPanelAndAnimateAppearance = false
|
||||||
if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) {
|
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 inputPanelNode !== self.inputPanelNode {
|
||||||
if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
|
if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
|
||||||
inputTextPanelNode.ensureUnfocused()
|
inputTextPanelNode.ensureUnfocused()
|
||||||
}
|
}
|
||||||
dismissedInputPanelNode = self.inputPanelNode
|
dismissedInputPanelNode = self.inputPanelNode
|
||||||
immediatelyLayoutInputPanelAndAnimateAppearance = true
|
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.inputPanelNode = inputPanelNode
|
||||||
self.insertSubnode(inputPanelNode, belowSubnode: self.navigateToLatestButton)
|
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 {
|
} else {
|
||||||
dismissedInputPanelNode = self.inputPanelNode
|
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)
|
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.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)
|
transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame)
|
||||||
|
|
||||||
if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) {
|
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.updateFrame(node: inputPanelNode, frame: inputPanelFrame)
|
||||||
transition.updateAlpha(node: inputPanelNode, alpha: 1.0)
|
transition.updateAlpha(node: inputPanelNode, alpha: 1.0)
|
||||||
inputPanelNode.updateFrames(transition: transition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let accessoryPanelNode = self.accessoryPanelNode, let accessoryPanelFrame = accessoryPanelFrame, !accessoryPanelNode.frame.equalTo(accessoryPanelFrame) {
|
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 {
|
if let dismissedInputPanelNode = dismissedInputPanelNode {
|
||||||
var frameCompleted = false
|
var frameCompleted = false
|
||||||
var alphaCompleted = false
|
var alphaCompleted = false
|
||||||
@@ -339,14 +408,34 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
completed()
|
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 {
|
if let textInputPanelNode = self.textInputPanelNode {
|
||||||
self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) }
|
self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.chatPresentationInterfaceState != chatPresentationInterfaceState {
|
if self.chatPresentationInterfaceState != chatPresentationInterfaceState {
|
||||||
|
var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState)
|
||||||
var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState
|
var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState
|
||||||
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
||||||
|
|
||||||
@@ -354,8 +443,28 @@ class ChatControllerNode: ASDisplayNode {
|
|||||||
textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState
|
textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.ignoreUpdateHeight {
|
let layoutTransition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||||
self.requestLayout(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) {
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if recognizer.state == .ended {
|
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
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode {
|
|||||||
private var accountAndFile: (Account, TelegramMediaFile)?
|
private var accountAndFile: (Account, TelegramMediaFile)?
|
||||||
private let dataDisposable = MetaDisposable()
|
private let dataDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var isVisible = false
|
private var itemIsVisible = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
if #available(iOS 9.0, *) {
|
if #available(iOS 9.0, *) {
|
||||||
@@ -93,7 +93,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode {
|
|||||||
if let fileName = file.fileName {
|
if let fileName = file.fileName {
|
||||||
pathExtension = (fileName as NSString).pathExtension
|
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
|
|> deliverOnMainQueue
|
||||||
self.dataDisposable.set(data.start(next: { [weak self] data in
|
self.dataDisposable.set(data.start(next: { [weak self] data in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
|||||||
@@ -284,18 +284,18 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
|||||||
|
|
||||||
var updateLayout: GridNodeUpdateLayout?
|
var updateLayout: GridNodeUpdateLayout?
|
||||||
if let updateSizeAndInsets = updateSizeAndInsets {
|
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 {
|
} 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) {
|
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 {
|
if !self.dequeuedInitialTransitionOnLayout {
|
||||||
self.dequeuedInitialTransitionOnLayout = true
|
self.dequeuedInitialTransitionOnLayout = true
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
|
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.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false)
|
||||||
self.zoomableContent = (largestSize.dimensions, self.imageNode)
|
self.zoomableContent = (largestSize.dimensions, self.imageNode)
|
||||||
|
self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource).start())
|
||||||
self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(CloudFileMediaResource(location: largestSize.location, size: largestSize.size ?? 0)).start())
|
|
||||||
} else {
|
} else {
|
||||||
self._ready.set(.single(Void()))
|
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 let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile {
|
||||||
if isVisible {
|
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 {
|
} else {
|
||||||
self.fetchDisposable.set(nil)
|
self.fetchDisposable.set(nil)
|
||||||
}
|
}
|
||||||
|
|||||||
11
TelegramUI/ChatInputNode.swift
Normal file
11
TelegramUI/ChatInputNode.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ import TelegramCore
|
|||||||
class ChatInputPanelNode: ASDisplayNode {
|
class ChatInputPanelNode: ASDisplayNode {
|
||||||
var account: Account?
|
var account: Account?
|
||||||
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||||
var peer: Peer?
|
|
||||||
|
|
||||||
func updateFrames(transition: ContainedViewLayoutTransition) {
|
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat {
|
||||||
|
return 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,16 @@ func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceSta
|
|||||||
}
|
}
|
||||||
return nil
|
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: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
21
TelegramUI/ChatInterfaceInputNodes.swift
Normal file
21
TelegramUI/ChatInterfaceInputNodes.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -7,12 +7,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||||||
if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode {
|
if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode {
|
||||||
currentPanel.selectedMessageCount = selectionState.selectedIds.count
|
currentPanel.selectedMessageCount = selectionState.selectedIds.count
|
||||||
currentPanel.interfaceInteraction = interfaceInteraction
|
currentPanel.interfaceInteraction = interfaceInteraction
|
||||||
currentPanel.peer = chatPresentationInterfaceState.peer
|
|
||||||
return currentPanel
|
return currentPanel
|
||||||
} else {
|
} else {
|
||||||
let panel = ChatMessageSelectionInputPanelNode()
|
let panel = ChatMessageSelectionInputPanelNode()
|
||||||
panel.account = account
|
panel.account = account
|
||||||
panel.peer = chatPresentationInterfaceState.peer
|
|
||||||
panel.selectedMessageCount = selectionState.selectedIds.count
|
panel.selectedMessageCount = selectionState.selectedIds.count
|
||||||
panel.interfaceInteraction = interfaceInteraction
|
panel.interfaceInteraction = interfaceInteraction
|
||||||
return panel
|
return panel
|
||||||
@@ -27,12 +25,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||||||
break
|
break
|
||||||
case .member:
|
case .member:
|
||||||
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
|
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
|
||||||
currentPanel.peer = peer
|
|
||||||
return currentPanel
|
return currentPanel
|
||||||
} else {
|
} else {
|
||||||
let panel = ChatChannelSubscriberInputPanelNode()
|
let panel = ChatChannelSubscriberInputPanelNode()
|
||||||
panel.account = account
|
panel.account = account
|
||||||
panel.peer = peer
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,12 +36,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||||||
switch channel.participationStatus {
|
switch channel.participationStatus {
|
||||||
case .kicked, .left:
|
case .kicked, .left:
|
||||||
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
|
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
|
||||||
currentPanel.peer = peer
|
|
||||||
return currentPanel
|
return currentPanel
|
||||||
} else {
|
} else {
|
||||||
let panel = ChatChannelSubscriberInputPanelNode()
|
let panel = ChatChannelSubscriberInputPanelNode()
|
||||||
panel.account = account
|
panel.account = account
|
||||||
panel.peer = peer
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
case .member:
|
case .member:
|
||||||
@@ -56,19 +50,16 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
|||||||
|
|
||||||
if let currentPanel = currentPanel as? ChatTextInputPanelNode {
|
if let currentPanel = currentPanel as? ChatTextInputPanelNode {
|
||||||
currentPanel.interfaceInteraction = interfaceInteraction
|
currentPanel.interfaceInteraction = interfaceInteraction
|
||||||
currentPanel.peer = peer
|
|
||||||
return currentPanel
|
return currentPanel
|
||||||
} else {
|
} else {
|
||||||
if let textInputPanelNode = textInputPanelNode {
|
if let textInputPanelNode = textInputPanelNode {
|
||||||
textInputPanelNode.interfaceInteraction = interfaceInteraction
|
textInputPanelNode.interfaceInteraction = interfaceInteraction
|
||||||
textInputPanelNode.account = account
|
textInputPanelNode.account = account
|
||||||
textInputPanelNode.peer = peer
|
|
||||||
return textInputPanelNode
|
return textInputPanelNode
|
||||||
} else {
|
} else {
|
||||||
let panel = ChatTextInputPanelNode()
|
let panel = ChatTextInputPanelNode()
|
||||||
panel.interfaceInteraction = interfaceInteraction
|
panel.interfaceInteraction = interfaceInteraction
|
||||||
panel.account = account
|
panel.account = account
|
||||||
panel.peer = peer
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Display
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import Photos
|
||||||
|
|
||||||
final class ChatMediaActionSheetController: ActionSheetController {
|
final class ChatMediaActionSheetController: ActionSheetController {
|
||||||
private let _ready = Promise<Bool>()
|
private let _ready = Promise<Bool>()
|
||||||
@@ -11,6 +12,7 @@ final class ChatMediaActionSheetController: ActionSheetController {
|
|||||||
}
|
}
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
|
|
||||||
|
var photo: (PHAsset) -> Void = { _ in }
|
||||||
var location: () -> Void = { }
|
var location: () -> Void = { }
|
||||||
var contacts: () -> Void = { }
|
var contacts: () -> Void = { }
|
||||||
|
|
||||||
@@ -21,7 +23,12 @@ final class ChatMediaActionSheetController: ActionSheetController {
|
|||||||
|
|
||||||
self.setItemGroups([
|
self.setItemGroups([
|
||||||
ActionSheetItemGroup(items: [
|
ActionSheetItemGroup(items: [
|
||||||
ChatMediaActionSheetRollItem(),
|
ChatMediaActionSheetRollItem(assetSelected: { [weak self] asset in
|
||||||
|
if let strongSelf = self {
|
||||||
|
self?.dismissAnimated()
|
||||||
|
strongSelf.photo(asset)
|
||||||
|
}
|
||||||
|
}),
|
||||||
ActionSheetButtonItem(title: "File", action: {}),
|
ActionSheetButtonItem(title: "File", action: {}),
|
||||||
ActionSheetButtonItem(title: "Location", action: { [weak self] in
|
ActionSheetButtonItem(title: "Location", action: { [weak self] in
|
||||||
self?.dismissAnimated()
|
self?.dismissAnimated()
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ import Photos
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
final class ChatMediaActionSheetRollItem: ActionSheetItem {
|
final class ChatMediaActionSheetRollItem: ActionSheetItem {
|
||||||
|
private let assetSelected: (PHAsset) -> Void
|
||||||
|
|
||||||
|
init(assetSelected: @escaping (PHAsset) -> Void) {
|
||||||
|
self.assetSelected = assetSelected
|
||||||
|
}
|
||||||
|
|
||||||
func node() -> ActionSheetItemNode {
|
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 assetCollection: PHAssetCollection?
|
||||||
private var fetchResult: PHFetchResult<PHAsset>?
|
private var fetchResult: PHFetchResult<PHAsset>?
|
||||||
|
|
||||||
override init() {
|
private let assetSelected: (PHAsset) -> Void
|
||||||
|
|
||||||
|
init(assetSelected: @escaping (PHAsset) -> Void) {
|
||||||
|
self.assetSelected = assetSelected
|
||||||
|
|
||||||
self.listView = ListView()
|
self.listView = ListView()
|
||||||
self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
|
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 {
|
if let fetchResult = self.fetchResult {
|
||||||
for i in 0 ..< fetchResult.count {
|
for i in 0 ..< fetchResult.count {
|
||||||
let asset = fetchResult.object(at: i)
|
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)
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
TelegramUI/ChatMediaInputGridEntries.swift
Normal file
39
TelegramUI/ChatMediaInputGridEntries.swift
Normal file
@@ -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: { })
|
||||||
|
}
|
||||||
|
}
|
||||||
322
TelegramUI/ChatMediaInputNode.swift
Normal file
322
TelegramUI/ChatMediaInputNode.swift
Normal file
@@ -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<ChatMediaInputGridEntryStableId>()
|
||||||
|
for entry in fromEntries {
|
||||||
|
fromStableIds.insert(entry.stableId)
|
||||||
|
}
|
||||||
|
var index = 0
|
||||||
|
var indices = Set<Int>()
|
||||||
|
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<StickerPacksCollectionPosition>()
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
70
TelegramUI/ChatMediaInputPanelEntries.swift
Normal file
70
TelegramUI/ChatMediaInputPanelEntries.swift
Normal file
@@ -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: {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
TelegramUI/ChatMediaInputStickerGridItem.swift
Normal file
163
TelegramUI/ChatMediaInputStickerGridItem.swift
Normal file
@@ -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)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
115
TelegramUI/ChatMediaInputStickerPackItem.swift
Normal file
115
TelegramUI/ChatMediaInputStickerPackItem.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
|||||||
private var progressNode: RadialProgressNode?
|
private var progressNode: RadialProgressNode?
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
|
private var account: Account?
|
||||||
|
private var messageIdAndFlags: (MessageId, MessageFlags)?
|
||||||
private var media: Media?
|
private var media: Media?
|
||||||
|
|
||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
@@ -48,6 +50,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
|||||||
if let fetchStatus = self.fetchStatus {
|
if let fetchStatus = self.fetchStatus {
|
||||||
switch fetchStatus {
|
switch fetchStatus {
|
||||||
case .Fetching:
|
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 }) {
|
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||||
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 currentMedia = self.media
|
||||||
let imageLayout = self.imageNode.asyncLayout()
|
let imageLayout = self.imageNode.asyncLayout()
|
||||||
|
|
||||||
return { account, media, corners, automaticDownload, constrainedSize in
|
return { account, message, media, corners, automaticDownload, constrainedSize in
|
||||||
var initialBoundingSize: CGSize
|
var initialBoundingSize: CGSize
|
||||||
var nativeSize: CGSize
|
var nativeSize: CGSize
|
||||||
|
|
||||||
@@ -113,10 +121,15 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
|||||||
mediaUpdated = true
|
mediaUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var statusUpdated = mediaUpdated
|
||||||
|
if currentMessageIdAndFlags?.0 != message.id || currentMessageIdAndFlags?.1 != message.flags {
|
||||||
|
statusUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
if mediaUpdated {
|
if mediaUpdated {
|
||||||
if let image = media as? TelegramMediaImage {
|
if let image = media as? TelegramMediaImage {
|
||||||
updateImageSignal = chatMessagePhoto(account: account, photo: image)
|
updateImageSignal = chatMessagePhoto(account: account, photo: image)
|
||||||
updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image)
|
|
||||||
updatedFetchControls = FetchControls(fetch: { [weak self] in
|
updatedFetchControls = FetchControls(fetch: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
|
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
|
||||||
@@ -126,7 +139,6 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
|||||||
})
|
})
|
||||||
} else if let file = media as? TelegramMediaFile {
|
} else if let file = media as? TelegramMediaFile {
|
||||||
updateImageSignal = chatMessageVideo(account: account, video: file)
|
updateImageSignal = chatMessageVideo(account: account, video: file)
|
||||||
updatedStatusSignal = chatMessageFileStatus(account: account, file: file)
|
|
||||||
updatedFetchControls = FetchControls(fetch: { [weak self] in
|
updatedFetchControls = FetchControls(fetch: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start())
|
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 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)
|
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
|
return (CGSize(width: adjustedImageSize.width, height: adjustedImageSize.height), { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
strongSelf.account = account
|
||||||
|
strongSelf.messageIdAndFlags = (message.id, message.flags)
|
||||||
strongSelf.media = media
|
strongSelf.media = media
|
||||||
strongSelf.imageNode.frame = adjustedImageFrame
|
strongSelf.imageNode.frame = adjustedImageFrame
|
||||||
strongSelf.progressNode?.position = CGPoint(x: adjustedImageFrame.midX, y: adjustedImageFrame.midY)
|
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()
|
let currentAsyncLayout = node?.asyncLayout()
|
||||||
|
|
||||||
return { account, media, corners, automaticDownload, constrainedSize in
|
return { account, message, media, corners, automaticDownload, constrainedSize in
|
||||||
var imageNode: ChatMessageInteractiveMediaNode
|
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 {
|
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||||
imageNode = node
|
imageNode = node
|
||||||
@@ -240,7 +280,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
|||||||
imageLayout = imageNode.asyncLayout()
|
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
|
return (initialWidth, { constrainedSize in
|
||||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
||||||
|
|||||||
@@ -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 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
|
return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in
|
||||||
let (refinedWidth, finishLayout) = refineLayout(constrainedSize)
|
let (refinedWidth, finishLayout) = refineLayout(constrainedSize)
|
||||||
|
|||||||
@@ -8,32 +8,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
|||||||
private let deleteButton: UIButton
|
private let deleteButton: UIButton
|
||||||
private let forwardButton: UIButton
|
private let forwardButton: UIButton
|
||||||
|
|
||||||
override var peer: Peer? {
|
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedMessageCount: Int = 0 {
|
var selectedMessageCount: Int = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -63,19 +38,6 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
|||||||
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside])
|
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() {
|
@objc func deleteButtonPressed() {
|
||||||
self.interfaceInteraction?.deleteSelectedMessages()
|
self.interfaceInteraction?.deleteSelectedMessages()
|
||||||
}
|
}
|
||||||
@@ -83,4 +45,38 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
|||||||
@objc func forwardButtonPressed() {
|
@objc func forwardButtonPressed() {
|
||||||
self.interfaceInteraction?.forwardSelectedMessages()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
if self.telegramFile != telegramFile {
|
if self.telegramFile != telegramFile {
|
||||||
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.imageNode.setSignal(account: item.account, signal: signal)
|
||||||
self.fetchDisposable.set(fileInteractiveFetched(account: item.account, file: telegramFile).start())
|
self.fetchDisposable.set(fileInteractiveFetched(account: item.account, file: telegramFile).start())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
if let file = webpage.file {
|
if let file = webpage.file {
|
||||||
if file.isVideo {
|
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
|
initialWidth = initialImageWidth + insets.left + insets.right
|
||||||
refineContentImageLayout = refineLayout
|
refineContentImageLayout = refineLayout
|
||||||
} else {
|
} else {
|
||||||
@@ -132,7 +132,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
} else if let image = webpage.image {
|
} else if let image = webpage.image {
|
||||||
if let type = webpage.type, ["photo"].contains(type) {
|
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
|
initialWidth = initialImageWidth + insets.left + insets.right
|
||||||
refineContentImageLayout = refineLayout
|
refineContentImageLayout = refineLayout
|
||||||
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ final class ChatPanelInterfaceInteraction {
|
|||||||
let deleteSelectedMessages: () -> Void
|
let deleteSelectedMessages: () -> Void
|
||||||
let forwardSelectedMessages: () -> Void
|
let forwardSelectedMessages: () -> Void
|
||||||
let updateTextInputState: (ChatTextInputState) -> 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.setupReplyMessage = setupReplyMessage
|
||||||
self.beginMessageSelection = beginMessageSelection
|
self.beginMessageSelection = beginMessageSelection
|
||||||
self.deleteSelectedMessages = deleteSelectedMessages
|
self.deleteSelectedMessages = deleteSelectedMessages
|
||||||
self.forwardSelectedMessages = forwardSelectedMessages
|
self.forwardSelectedMessages = forwardSelectedMessages
|
||||||
self.updateTextInputState = updateTextInputState
|
self.updateTextInputState = updateTextInputState
|
||||||
|
self.updateInputMode = updateInputMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,33 @@ enum ChatPresentationInputContext {
|
|||||||
case mention
|
case mention
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ChatInputMode {
|
||||||
|
case none
|
||||||
|
case text
|
||||||
|
case media
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatPresentationInterfaceState: Equatable {
|
struct ChatPresentationInterfaceState: Equatable {
|
||||||
let interfaceState: ChatInterfaceState
|
let interfaceState: ChatInterfaceState
|
||||||
let peer: Peer?
|
let peer: Peer?
|
||||||
|
let inputTextPanelState: ChatTextInputPanelState
|
||||||
let inputContext: ChatPresentationInputContext?
|
let inputContext: ChatPresentationInputContext?
|
||||||
|
let inputMode: ChatInputMode
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.interfaceState = ChatInterfaceState()
|
self.interfaceState = ChatInterfaceState()
|
||||||
|
self.inputTextPanelState = ChatTextInputPanelState()
|
||||||
self.peer = nil
|
self.peer = nil
|
||||||
self.inputContext = 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.interfaceState = interfaceState
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.inputTextPanelState = inputTextPanelState
|
||||||
self.inputContext = inputContext
|
self.inputContext = inputContext
|
||||||
|
self.inputMode = inputMode
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool {
|
static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool {
|
||||||
@@ -35,22 +47,38 @@ struct ChatPresentationInterfaceState: Equatable {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lhs.inputTextPanelState != rhs.inputTextPanelState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if lhs.inputContext != rhs.inputContext {
|
if lhs.inputContext != rhs.inputContext {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lhs.inputMode != rhs.inputMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState {
|
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 {
|
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 {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import Postbox
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
private let textInputViewBackground: UIImage = {
|
private let textInputViewBackground: UIImage = {
|
||||||
let diameter: CGFloat = 10.0
|
let diameter: CGFloat = 35.0
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0)
|
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0)
|
||||||
let context = UIGraphicsGetCurrentContext()!
|
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.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
||||||
context.setFillColor(UIColor.white.cgColor)
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
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
|
let strokeWidth: CGFloat = 0.5
|
||||||
context.setLineWidth(strokeWidth)
|
context.setLineWidth(strokeWidth)
|
||||||
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - 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
|
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 {
|
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||||
var textPlaceholderNode: TextNode
|
var textPlaceholderNode: TextNode
|
||||||
var textInputNode: ASEditableTextNode?
|
var textInputNode: ASEditableTextNode?
|
||||||
|
|
||||||
let textInputBackgroundView: UIImageView
|
let textInputBackgroundView: UIImageView
|
||||||
|
let micButton: UIButton
|
||||||
let sendButton: UIButton
|
let sendButton: UIButton
|
||||||
let attachmentButton: UIButton
|
let attachmentButton: UIButton
|
||||||
|
|
||||||
|
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
|
||||||
|
|
||||||
var displayAttachmentMenu: () -> Void = { }
|
var displayAttachmentMenu: () -> Void = { }
|
||||||
var sendMessage: () -> Void = { }
|
var sendMessage: () -> Void = { }
|
||||||
var updateHeight: () -> Void = { }
|
var updateHeight: () -> Void = { }
|
||||||
@@ -40,25 +99,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
private var updatingInputState = false
|
private var updatingInputState = false
|
||||||
|
|
||||||
private var currentPlaceholder: String?
|
private var currentPlaceholder: String?
|
||||||
override var peer: Peer? {
|
|
||||||
didSet {
|
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var inputTextState: ChatTextInputState {
|
var inputTextState: ChatTextInputState {
|
||||||
get {
|
get {
|
||||||
@@ -72,7 +114,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
} set(value) {
|
} set(value) {
|
||||||
if let textInputNode = self.textInputNode {
|
if let textInputNode = self.textInputNode {
|
||||||
self.updatingInputState = true
|
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)
|
textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count)
|
||||||
self.updatingInputState = false
|
self.updatingInputState = false
|
||||||
}
|
}
|
||||||
@@ -84,19 +126,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
return self.textInputNode?.attributedText?.string ?? ""
|
return self.textInputNode?.attributedText?.string ?? ""
|
||||||
} set(value) {
|
} set(value) {
|
||||||
if let textInputNode = self.textInputNode {
|
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)
|
self.editableTextNodeDidUpdateText(textInputNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let textFieldInsets = UIEdgeInsets(top: 9.0, left: 41.0, bottom: 8.0, right: 0.0)
|
let textFieldInsets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0)
|
||||||
let textInputViewInternalInsets = UIEdgeInsets(top: 4.0, left: 5.0, bottom: 4.0, right: 5.0)
|
let textInputViewInternalInsets = UIEdgeInsets(top: 6.5, left: 13.0, bottom: 7.5, right: 13.0)
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.textInputBackgroundView = UIImageView(image: textInputViewBackground)
|
self.textInputBackgroundView = UIImageView(image: textInputViewBackground)
|
||||||
self.textPlaceholderNode = TextNode()
|
self.textPlaceholderNode = TextNode()
|
||||||
|
self.textPlaceholderNode.isLayerBacked = true
|
||||||
self.attachmentButton = UIButton()
|
self.attachmentButton = UIButton()
|
||||||
|
self.micButton = UIButton()
|
||||||
self.sendButton = UIButton()
|
self.sendButton = UIButton()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@@ -105,24 +149,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside)
|
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside)
|
||||||
self.view.addSubview(self.attachmentButton)
|
self.view.addSubview(self.attachmentButton)
|
||||||
|
|
||||||
self.sendButton.titleLabel?.font = Font.medium(17.0)
|
self.micButton.setImage(micIcon, for: [])
|
||||||
self.sendButton.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 6.0, bottom: 8.0, right: 6.0)
|
self.micButton.addTarget(self, action: #selector(self.micButtonPressed), for: .touchUpInside)
|
||||||
self.sendButton.setTitleColor(UIColor(0x007ee5), for: [])
|
self.view.addSubview(self.micButton)
|
||||||
self.sendButton.setTitleColor(UIColor.gray, for: [.highlighted])
|
|
||||||
self.sendButton.setTitle("Send", for: [])
|
self.sendButton.setImage(sendIcon, for: [])
|
||||||
self.sendButton.sizeToFit()
|
|
||||||
self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside)
|
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)
|
self.view.addSubview(self.textInputBackgroundView)
|
||||||
|
|
||||||
let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode)
|
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)
|
self.textPlaceholderNode.frame = CGRect(origin: CGPoint(), size: placeholderSize.size)
|
||||||
let _ = placeholderApply()
|
let _ = placeholderApply()
|
||||||
self.addSubnode(self.textPlaceholderNode)
|
self.addSubnode(self.textPlaceholderNode)
|
||||||
|
|
||||||
self.view.addSubview(self.sendButton)
|
|
||||||
|
|
||||||
self.textInputBackgroundView.clipsToBounds = true
|
self.textInputBackgroundView.clipsToBounds = true
|
||||||
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
|
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
|
||||||
recognizer.touchDown = { [weak self] in
|
recognizer.touchDown = { [weak self] in
|
||||||
@@ -140,16 +183,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
|
|
||||||
private func loadTextInputNode() {
|
private func loadTextInputNode() {
|
||||||
let textInputNode = ASEditableTextNode()
|
let textInputNode = ASEditableTextNode()
|
||||||
textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)]
|
textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
|
||||||
textInputNode.clipsToBounds = true
|
textInputNode.clipsToBounds = true
|
||||||
textInputNode.delegate = self
|
textInputNode.delegate = self
|
||||||
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||||
self.addSubnode(textInputNode)
|
self.addSubnode(textInputNode)
|
||||||
self.textInputNode = 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 - 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 - 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)
|
|
||||||
|
|
||||||
self.textInputBackgroundView.isUserInteractionEnabled = false
|
self.textInputBackgroundView.isUserInteractionEnabled = false
|
||||||
self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0])
|
self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0])
|
||||||
@@ -163,55 +204,168 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
textInputNode.view.addGestureRecognizer(recognizer)
|
textInputNode.view.addGestureRecognizer(recognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat {
|
||||||
let sendButtonSize = self.sendButton.bounds.size
|
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
|
let textFieldHeight: CGFloat
|
||||||
if let textInputNode = self.textInputNode {
|
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 {
|
} 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)
|
let panelHeight = 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 sendButtonSize = self.sendButton.bounds.size
|
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
|
||||||
let minimalHeight: CGFloat = 45.0
|
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(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.sendButton.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.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: bounds.height - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
|
|
||||||
|
|
||||||
if let textInputNode = self.textInputNode {
|
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(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) {
|
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||||
if let textInputNode = self.textInputNode {
|
if let textInputNode = self.textInputNode {
|
||||||
self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0
|
self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0
|
||||||
|
|
||||||
let constrainedSize = CGSize(width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude)
|
if let text = self.textInputNode?.attributedText, text.length != 0 {
|
||||||
let sendButtonSize = self.sendButton.bounds.size
|
if self.sendButton.alpha.isZero {
|
||||||
|
self.sendButton.alpha = 1.0
|
||||||
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)))
|
self.micButton.alpha = 0.0
|
||||||
if abs(textFieldHeight - textInputNode.frame.size.height) > CGFloat(FLT_EPSILON) {
|
self.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||||
self.invalidateCalculatedLayout()
|
self.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
||||||
self.updateHeight()
|
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)
|
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() {
|
@objc func sendButtonPressed() {
|
||||||
let text = self.textInputNode?.attributedText?.string ?? ""
|
let text = self.textInputNode?.attributedText?.string ?? ""
|
||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
@@ -235,12 +403,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self.displayAttachmentMenu()
|
self.displayAttachmentMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func micButtonPressed() {
|
||||||
|
}
|
||||||
|
|
||||||
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
|
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
self.ensureFocused()
|
self.ensureFocused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFocused: Bool {
|
||||||
|
return self.textInputNode?.isFirstResponder() ?? false
|
||||||
|
}
|
||||||
|
|
||||||
func ensureUnfocused() {
|
func ensureUnfocused() {
|
||||||
self.textInputNode?.resignFirstResponder()
|
self.textInputNode?.resignFirstResponder()
|
||||||
}
|
}
|
||||||
@@ -267,11 +442,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/*override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView! {
|
@objc func accessoryItemButtonPressed(_ button: UIView) {
|
||||||
if let textInputNode = self.textInputNode where self.textInputBackgroundView.frame.contains(point) {
|
for (item, currentButton) in self.accessoryItemButtons {
|
||||||
return textInputNode.view
|
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)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
/*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size))
|
/*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size))
|
||||||
self.videoNode.player = VideoPlayer(source: source)*/
|
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)
|
player.attachPlayerNode(self.videoNode)
|
||||||
self.player = player
|
self.player = player
|
||||||
self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in
|
self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ public class ContactsController: ViewController {
|
|||||||
|
|
||||||
let interaction = ContactsControllerInteraction(openPeer: { [weak self] peerId in
|
let interaction = ContactsControllerInteraction(openPeer: { [weak self] peerId in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
strongSelf.contactsNode.listView.clearHighlightAnimated(true)
|
||||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
||||||
}
|
}
|
||||||
}, activateSearch: { [weak self] in
|
}, activateSearch: { [weak self] in
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
|
|||||||
|
|
||||||
var fetchedCount: Int32 = 0
|
var fetchedCount: Int32 = 0
|
||||||
|
|
||||||
let readCount = min(resource.size - context.readingOffset, Int(bufferSize))
|
let resourceSize: Int = resource.size ?? 0
|
||||||
let data = account.postbox.mediaBox.resourceData(resource, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete)
|
|
||||||
|
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?
|
var fetchedData: Data?
|
||||||
let semaphore = DispatchSemaphore(value: 0)
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
let _ = data.start(next: { data in
|
let _ = data.start(next: { data in
|
||||||
@@ -110,18 +112,20 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
|
|||||||
|
|
||||||
var result: Int64 = offset
|
var result: Int64 = offset
|
||||||
|
|
||||||
|
let resourceSize: Int = resource.size ?? 0
|
||||||
|
|
||||||
if (whence & AVSEEK_SIZE) != 0 {
|
if (whence & AVSEEK_SIZE) != 0 {
|
||||||
result = Int64(resource.size)
|
result = Int64(resourceSize)
|
||||||
} else {
|
} else {
|
||||||
context.readingOffset = Int(min(Int64(resource.size), offset))
|
context.readingOffset = Int(min(Int64(resourceSize), offset))
|
||||||
|
|
||||||
if context.readingOffset != context.requestedDataOffset {
|
if context.readingOffset != context.requestedDataOffset {
|
||||||
context.requestedDataOffset = context.readingOffset
|
context.requestedDataOffset = context.readingOffset
|
||||||
|
|
||||||
if context.readingOffset >= resource.size {
|
if context.readingOffset >= resourceSize {
|
||||||
context.fetchedDataDisposable.set(nil)
|
context.fetchedDataDisposable.set(nil)
|
||||||
} else {
|
} 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.account = account
|
||||||
self.resource = resource
|
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()
|
var avFormatContextRef = avformat_alloc_context()
|
||||||
guard let avFormatContext = avFormatContextRef else {
|
guard let avFormatContext = avFormatContextRef else {
|
||||||
|
|||||||
90
TelegramUI/FetchCachedRepresentations.swift
Normal file
90
TelegramUI/FetchCachedRepresentations.swift
Normal file
@@ -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<CachedMediaResourceRepresentationResult, NoError> {
|
||||||
|
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<CachedMediaResourceRepresentationResult, NoError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -3,14 +3,10 @@ import Postbox
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
func fileResource(_ file: TelegramMediaFile) -> CloudFileMediaResource {
|
|
||||||
return CloudFileMediaResource(location: file.location, size: file.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal<Void, NoError> {
|
func fileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal<Void, NoError> {
|
||||||
return account.postbox.mediaBox.fetchedResource(fileResource(file))
|
return account.postbox.mediaBox.fetchedResource(file.resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) {
|
func fileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) {
|
||||||
account.postbox.mediaBox.cancelInteractiveResourceFetch(fileResource(file))
|
account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class GalleryController: ViewController {
|
|||||||
|
|
||||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.donePressed))
|
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)
|
let message = account.postbox.messageAtId(messageId)
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ class GalleryController: ViewController {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
switch style {
|
switch style {
|
||||||
case .dark:
|
case .dark:
|
||||||
strongSelf.statusBar.style = .White
|
strongSelf.statusBar.statusBarStyle = .White
|
||||||
strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||||
strongSelf.navigationBar.stripeColor = UIColor.clear
|
strongSelf.navigationBar.stripeColor = UIColor.clear
|
||||||
strongSelf.navigationBar.foregroundColor = UIColor.white
|
strongSelf.navigationBar.foregroundColor = UIColor.white
|
||||||
@@ -212,7 +212,7 @@ class GalleryController: ViewController {
|
|||||||
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black
|
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black
|
||||||
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true
|
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true
|
||||||
case .light:
|
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.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
|
||||||
strongSelf.navigationBar.foregroundColor = UIColor.black
|
strongSelf.navigationBar.foregroundColor = UIColor.black
|
||||||
strongSelf.navigationBar.accentColor = UIColor(0x007ee5)
|
strongSelf.navigationBar.accentColor = UIColor(0x007ee5)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import Foundation
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
|
|
||||||
class GridHoleItem: GridItem {
|
final class GridHoleItem: GridItem {
|
||||||
|
let section: GridSection? = nil
|
||||||
|
|
||||||
func node(layout: GridNodeLayout) -> GridItemNode {
|
func node(layout: GridNodeLayout) -> GridItemNode {
|
||||||
return GridHoleItemNode()
|
return GridHoleItemNode()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ final class GridMessageItem: GridItem {
|
|||||||
private let message: Message
|
private let message: Message
|
||||||
private let controllerInteraction: ChatControllerInteraction
|
private let controllerInteraction: ChatControllerInteraction
|
||||||
|
|
||||||
|
let section: GridSection? = nil
|
||||||
|
|
||||||
init(account: Account, message: Message, controllerInteraction: ChatControllerInteraction) {
|
init(account: Account, message: Message, controllerInteraction: ChatControllerInteraction) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ enum GroupInfoEntry: PeerInfoEntry {
|
|||||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||||
switch self {
|
switch self {
|
||||||
case let .info(peer, cachedData):
|
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:
|
case .setGroupPhoto:
|
||||||
return PeerInfoActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .blocks, action: {
|
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"
|
label = "Enabled"
|
||||||
}
|
}
|
||||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .blocks, action: {
|
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .blocks, action: {
|
||||||
interaction.changeNotificationNoteSettings()
|
interaction.changeNotificationMuteSettings()
|
||||||
})
|
})
|
||||||
case .sharedMedia:
|
case .sharedMedia:
|
||||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .blocks, action: {
|
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] = []
|
var entries: [PeerInfoEntry] = []
|
||||||
entries.append(GroupInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<UIImage, NoError>? {
|
func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal<UIImage, NoError>? {
|
||||||
if let location = peer.smallProfileImage?.location.cloudLocation {
|
if let smallProfileImage = peer.smallProfileImage {
|
||||||
return deferred { () -> Signal<UIImage, NoError> in
|
let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource)
|
||||||
return cachedCloudFileLocation(location)
|
let imageData = resourceData
|
||||||
|> `catch` { _ in
|
|> take(1)
|
||||||
return multipartDownloadFromCloudLocation(account: account, location: location, size: nil)
|
|> mapToSignal { maybeData -> Signal<Data?, NoError> in
|
||||||
|> afterNext { data in
|
if maybeData.complete {
|
||||||
cacheCloudFileLocation(location, data: data)
|
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 {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,16 +247,10 @@ class PeerInfoActionItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,25 @@ import Postbox
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
struct PeerInfoAvatarAndNameItemEditingState: Equatable {
|
||||||
|
static func ==(lhs: PeerInfoAvatarAndNameItemEditingState, rhs: PeerInfoAvatarAndNameItemEditingState) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem {
|
class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem {
|
||||||
let account: Account
|
let account: Account
|
||||||
let peer: Peer?
|
let peer: Peer?
|
||||||
let cachedData: CachedPeerData?
|
let cachedData: CachedPeerData?
|
||||||
|
let editingState: PeerInfoAvatarAndNameItemEditingState?
|
||||||
let sectionId: PeerInfoItemSectionId
|
let sectionId: PeerInfoItemSectionId
|
||||||
let style: PeerInfoListStyle
|
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.account = account
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.cachedData = cachedData
|
self.cachedData = cachedData
|
||||||
|
self.editingState = editingState
|
||||||
self.sectionId = sectionId
|
self.sectionId = sectionId
|
||||||
self.style = style
|
self.style = style
|
||||||
}
|
}
|
||||||
@@ -29,13 +37,17 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem {
|
|||||||
node.insets = layout.insets
|
node.insets = layout.insets
|
||||||
|
|
||||||
completion(node, {
|
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) {
|
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 {
|
if let node = node as? PeerInfoAvatarAndNameItemNode {
|
||||||
|
var animated = true
|
||||||
|
if case .None = animation {
|
||||||
|
animated = false
|
||||||
|
}
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
let makeLayout = node.asyncLayout()
|
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))
|
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
completion(layout, {
|
completion(layout, {
|
||||||
apply()
|
apply(animated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +82,10 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
|||||||
private let nameNode: TextNode
|
private let nameNode: TextNode
|
||||||
private let statusNode: TextNode
|
private let statusNode: TextNode
|
||||||
|
|
||||||
|
private var inputSeparator: ASDisplayNode?
|
||||||
|
private var inputFirstField: ASEditableTextNode?
|
||||||
|
private var inputSecondField: ASEditableTextNode?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.isLayerBacked = true
|
self.backgroundNode.isLayerBacked = true
|
||||||
@@ -102,7 +118,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
|||||||
self.addSubnode(self.statusNode)
|
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 layoutNameNode = TextNode.asyncLayout(self.nameNode)
|
||||||
let layoutStatusNode = TextNode.asyncLayout(self.statusNode)
|
let layoutStatusNode = TextNode.asyncLayout(self.statusNode)
|
||||||
|
|
||||||
@@ -161,7 +177,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
|||||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
let layoutSize = layout.size
|
let layoutSize = layout.size
|
||||||
|
|
||||||
return (layout, { [weak self] in
|
return (layout, { [weak self] animated in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let avatarOriginY: CGFloat
|
let avatarOriginY: CGFloat
|
||||||
switch item.style {
|
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.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)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import SwiftSignalKit
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
final class PeerInfoControllerInteraction {
|
final class PeerInfoControllerInteraction {
|
||||||
|
let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void
|
||||||
let openSharedMedia: () -> Void
|
let openSharedMedia: () -> Void
|
||||||
let changeNotificationNoteSettings: () -> Void
|
let changeNotificationMuteSettings: () -> Void
|
||||||
let openPeerInfo: (PeerId) -> 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.openSharedMedia = openSharedMedia
|
||||||
self.changeNotificationNoteSettings = changeNotificationNoteSettings
|
self.changeNotificationMuteSettings = changeNotificationMuteSettings
|
||||||
self.openPeerInfo = openPeerInfo
|
self.openPeerInfo = openPeerInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,8 +63,16 @@ private func preparedPeerInfoEntryTransition(account: Account, from fromEntries:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PeerInfoEquatableState: Equatable {
|
private struct PeerInfoEquatableState: Equatable {
|
||||||
|
let state: PeerInfoState?
|
||||||
|
|
||||||
static func ==(lhs: PeerInfoEquatableState, rhs: PeerInfoEquatableState) -> Bool {
|
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 let changeSettingsDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var currentListStyle: PeerInfoListStyle = .plain
|
private var currentListStyle: PeerInfoListStyle = .plain
|
||||||
private var state = Promise<PeerInfoState?>(nil)
|
|
||||||
|
private var state = PeerInfoEquatableState(state: nil) {
|
||||||
|
didSet {
|
||||||
|
self.statePromise.set(.single(self.state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var statePromise = Promise<PeerInfoEquatableState>(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) {
|
public init(account: Account, peerId: PeerId) {
|
||||||
self.account = account
|
self.account = account
|
||||||
@@ -103,13 +124,17 @@ public final class PeerInfoController: ListController {
|
|||||||
override public func displayNodeDidLoad() {
|
override public func displayNodeDidLoad() {
|
||||||
super.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 strongSelf = self {
|
||||||
if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) {
|
if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) {
|
||||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, changeNotificationNoteSettings: { [weak self] in
|
}, changeNotificationMuteSettings: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let controller = ActionSheetController()
|
let controller = ActionSheetController()
|
||||||
let dismissAction: () -> Void = { [weak controller] in
|
let dismissAction: () -> Void = { [weak controller] in
|
||||||
@@ -167,9 +192,11 @@ public final class PeerInfoController: ListController {
|
|||||||
let previousEntries = Atomic<[PeerInfoSortableEntry]?>(value: nil)
|
let previousEntries = Atomic<[PeerInfoSortableEntry]?>(value: nil)
|
||||||
|
|
||||||
let account = self.account
|
let account = self.account
|
||||||
let transition = account.viewTracker.peerView(self.peerId)
|
let transition = combineLatest(account.viewTracker.peerView(self.peerId), self.statePromise.get()
|
||||||
|> map { view -> (PeerInfoEntryTransition, PeerInfoListStyle, Bool, Bool) in
|
|> distinctUntilChanged)
|
||||||
let entries = peerInfoEntries(view: view).map { PeerInfoSortableEntry(entry: $0) }
|
|> 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())
|
assert(entries == entries.sorted())
|
||||||
let previous = previousEntries.swap(entries)
|
let previous = previousEntries.swap(entries)
|
||||||
let style: PeerInfoListStyle
|
let style: PeerInfoListStyle
|
||||||
@@ -180,12 +207,49 @@ public final class PeerInfoController: ListController {
|
|||||||
} else {
|
} else {
|
||||||
style = .plain
|
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
|
|> deliverOnMainQueue
|
||||||
|
|
||||||
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated) in
|
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated, leftButton, rightButton) in
|
||||||
self?.enqueueTransition(transition, style: style, firstTime: firstTime, animated: animated)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,18 +244,10 @@ class PeerInfoDisclosureItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,21 +38,24 @@ protocol PeerInfoEntry {
|
|||||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem
|
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PeerInfoNavigationButton {
|
struct PeerInfoNavigationButton {
|
||||||
case none
|
let title: String
|
||||||
case edit
|
let action: (PeerInfoState?) -> PeerInfoState?
|
||||||
case done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol PeerInfoState {
|
protocol PeerInfoState {
|
||||||
func isEqual(to: PeerInfoState) -> Bool
|
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 {
|
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 {
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
||||||
switch channel.info {
|
switch channel.info {
|
||||||
case .broadcast:
|
case .broadcast:
|
||||||
@@ -63,5 +66,5 @@ func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
|||||||
} else if let group = view.peers[view.peerId] as? TelegramGroup {
|
} else if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||||
return groupInfoEntries(view: view)
|
return groupInfoEntries(view: view)
|
||||||
}
|
}
|
||||||
return []
|
return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,18 +206,10 @@ class PeerInfoPeerActionItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,22 +300,10 @@ class PeerInfoPeerItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.backgroundNode.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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,14 +111,10 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
||||||
self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public class PeerMediaCollectionController: ViewController {
|
|||||||
strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) })
|
strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}, sendSticker: { _ in })
|
||||||
|
|
||||||
self.controllerInteraction = controllerInteraction
|
self.controllerInteraction = controllerInteraction
|
||||||
|
|
||||||
@@ -184,7 +184,9 @@ public class PeerMediaCollectionController: ViewController {
|
|||||||
|
|
||||||
}, forwardSelectedMessages: {
|
}, forwardSelectedMessages: {
|
||||||
|
|
||||||
}, updateTextInputState: { _ in })
|
}, updateTextInputState: { _ in
|
||||||
|
}, updateInputMode: { _ in
|
||||||
|
})
|
||||||
|
|
||||||
self.updateInterfaceState(animated: false, { return $0 })
|
self.updateInterfaceState(animated: false, { return $0 })
|
||||||
|
|
||||||
|
|||||||
@@ -74,21 +74,21 @@ class PeerMediaCollectionControllerNode: ASDisplayNode {
|
|||||||
insets.top += navigationBarHeight
|
insets.top += navigationBarHeight
|
||||||
|
|
||||||
if let selectionState = self.mediaCollectionInterfaceState.selectionState {
|
if let selectionState = self.mediaCollectionInterfaceState.selectionState {
|
||||||
|
let interfaceState = ChatPresentationInterfaceState().updatedPeer({ _ in self.mediaCollectionInterfaceState.peer })
|
||||||
|
|
||||||
if let selectionPanel = self.selectionPanel {
|
if let selectionPanel = self.selectionPanel {
|
||||||
selectionPanel.peer = self.mediaCollectionInterfaceState.peer
|
|
||||||
selectionPanel.selectedMessageCount = selectionState.selectedIds.count
|
selectionPanel.selectedMessageCount = selectionState.selectedIds.count
|
||||||
let panelSize = selectionPanel.measure(layout.size)
|
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 - panelSize.height), size: panelSize))
|
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 {
|
} else {
|
||||||
let selectionPanel = ChatMessageSelectionInputPanelNode()
|
let selectionPanel = ChatMessageSelectionInputPanelNode()
|
||||||
selectionPanel.peer = self.mediaCollectionInterfaceState.peer
|
|
||||||
selectionPanel.selectedMessageCount = selectionState.selectedIds.count
|
selectionPanel.selectedMessageCount = selectionState.selectedIds.count
|
||||||
selectionPanel.backgroundColor = UIColor(0xfafafa)
|
selectionPanel.backgroundColor = UIColor(0xfafafa)
|
||||||
let panelSize = selectionPanel.measure(layout.size)
|
let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: interfaceState)
|
||||||
selectionPanel.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom), size: panelSize)
|
|
||||||
self.selectionPanel = selectionPanel
|
self.selectionPanel = selectionPanel
|
||||||
self.addSubnode(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 {
|
} else if let selectionPanel = self.selectionPanel {
|
||||||
self.selectionPanel = nil
|
self.selectionPanel = nil
|
||||||
|
|||||||
@@ -11,25 +11,22 @@ func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMedia
|
|||||||
return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0))
|
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> {
|
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 smallestSize = smallestRepresentation.size, let largestSize = largestRepresentation.size {
|
if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) {
|
||||||
let thumbnailResource = CloudFileMediaResource(location: smallestRepresentation.location, size: smallestSize)
|
let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource)
|
||||||
let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize)
|
|
||||||
|
|
||||||
let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource)
|
let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in
|
||||||
|
if maybeData.complete {
|
||||||
let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Int), NoError> in
|
|
||||||
if maybeData.size >= fullSizeResource.size {
|
|
||||||
let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: [])
|
let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: [])
|
||||||
|
|
||||||
return .single((nil, loadedData, fullSizeResource.size))
|
return .single((nil, loadedData, true))
|
||||||
} else {
|
} else {
|
||||||
let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource)
|
let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource)
|
||||||
let fetchedFullSize = account.postbox.mediaBox.fetchedResource(fullSizeResource)
|
let fetchedFullSize = account.postbox.mediaBox.fetchedResource(largestRepresentation.resource)
|
||||||
|
|
||||||
let thumbnail = Signal<Data?, NoError> { subscriber in
|
let thumbnail = Signal<Data?, NoError> { subscriber in
|
||||||
let fetchedDisposable = fetchedThumbnail.start()
|
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: []))
|
subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))
|
||||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||||
|
|
||||||
@@ -39,13 +36,13 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullSizeData: Signal<Data?, NoError>
|
let fullSizeData: Signal<(Data?, Bool), NoError>
|
||||||
|
|
||||||
if autoFetchFullSize {
|
if autoFetchFullSize {
|
||||||
fullSizeData = Signal<Data?, NoError> { subscriber in
|
fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in
|
||||||
let fetchedFullSizeDisposable = fetchedFullSize.start()
|
let fetchedFullSizeDisposable = fetchedFullSize.start()
|
||||||
let fullSizeDisposable = account.postbox.mediaBox.resourceData(fullSizeResource).start(next: { next in
|
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: []))
|
subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete))
|
||||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||||
|
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
@@ -54,16 +51,16 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fullSizeData = account.postbox.mediaBox.resourceData(fullSizeResource)
|
fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource)
|
||||||
|> map { next -> Data? in
|
|> map { next -> (Data?, Bool) in
|
||||||
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])
|
return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return thumbnail |> mapToSignal { thumbnailData in
|
return thumbnail |> mapToSignal { thumbnailData in
|
||||||
return fullSizeData |> map { fullSizeData in
|
return fullSizeData |> map { (fullSizeData, complete) in
|
||||||
return (thumbnailData, fullSizeData, fullSizeResource.size)
|
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> {
|
private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> {
|
||||||
if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations), let smallestSize = smallestRepresentation.size {
|
if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations), let largestRepresentation = largestImageRepresentation(file.previewRepresentations) {
|
||||||
let thumbnailResource = CloudFileMediaResource(location: smallestRepresentation.location, size: smallestSize)
|
let thumbnailResource = smallestRepresentation.resource
|
||||||
let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size)
|
let fullSizeResource = largestRepresentation.resource
|
||||||
|
|
||||||
let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource)
|
let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource)
|
||||||
|
|
||||||
let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Int), NoError> in
|
let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Bool), NoError> in
|
||||||
if maybeData.size >= fullSizeResource.size {
|
if maybeData.complete {
|
||||||
let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: [])
|
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 {
|
} else {
|
||||||
let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource)
|
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)
|
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 thumbnail |> mapToSignal { thumbnailData in
|
||||||
return fullSizeDataAndPath |> map { dataAndPath in
|
return fullSizeDataAndPath |> map { (dataAndPath, complete) in
|
||||||
return (thumbnailData, dataAndPath, fullSizeResource.size)
|
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> {
|
func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
||||||
let signal = chatMessagePhotoDatas(account: account, photo: photo)
|
let signal = chatMessagePhotoDatas(account: account, photo: photo)
|
||||||
|
|
||||||
return signal |> map { (thumbnailData, fullSizeData, fullTotalSize) in
|
return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in
|
||||||
return { arguments in
|
return { arguments in
|
||||||
assertNotOnMainThread()
|
assertNotOnMainThread()
|
||||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||||
@@ -383,7 +380,7 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr
|
|||||||
|
|
||||||
var fullSizeImage: CGImage?
|
var fullSizeImage: CGImage?
|
||||||
if let fullSizeData = fullSizeData {
|
if let fullSizeData = fullSizeData {
|
||||||
if fullSizeData.count >= fullTotalSize {
|
if fullSizeComplete {
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String)
|
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)
|
options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String)
|
||||||
@@ -392,7 +389,7 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let imageSource = CGImageSourceCreateIncremental(nil)
|
let imageSource = CGImageSourceCreateIncremental(nil)
|
||||||
CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeData.count >= fullTotalSize)
|
CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete)
|
||||||
|
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options[kCGImageSourceShouldCache as NSString] = false as NSNumber
|
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> {
|
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)
|
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
|
return { arguments in
|
||||||
assertNotOnMainThread()
|
assertNotOnMainThread()
|
||||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||||
@@ -461,7 +458,7 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa
|
|||||||
|
|
||||||
var fullSizeImage: CGImage?
|
var fullSizeImage: CGImage?
|
||||||
if let fullSizeData = fullSizeData {
|
if let fullSizeData = fullSizeData {
|
||||||
if fullSizeData.count >= fullTotalSize {
|
if fullSizeComplete {
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String)
|
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)
|
options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String)
|
||||||
@@ -470,7 +467,7 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let imageSource = CGImageSourceCreateIncremental(nil)
|
let imageSource = CGImageSourceCreateIncremental(nil)
|
||||||
CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeData.count >= fullTotalSize)
|
CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete)
|
||||||
|
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options[kCGImageSourceShouldCache as NSString] = false as NSNumber
|
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<MediaResourceStatus, NoError> {
|
func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal<MediaResourceStatus, NoError> {
|
||||||
if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size {
|
if let largestRepresentation = largestRepresentationForPhoto(photo) {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize)
|
return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource)
|
||||||
return account.postbox.mediaBox.resourceStatus(fullSizeResource)
|
|
||||||
} else {
|
} else {
|
||||||
return .never()
|
return .never()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatMessagePhotoInteractiveFetched(account: Account, photo: TelegramMediaImage) -> Signal<Void, NoError> {
|
func chatMessagePhotoInteractiveFetched(account: Account, photo: TelegramMediaImage) -> Signal<Void, NoError> {
|
||||||
if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size {
|
if let largestRepresentation = largestRepresentationForPhoto(photo) {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize)
|
return account.postbox.mediaBox.fetchedResource(largestRepresentation.resource)
|
||||||
return account.postbox.mediaBox.fetchedResource(fullSizeResource)
|
|
||||||
} else {
|
} else {
|
||||||
return .never()
|
return .never()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) {
|
func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) {
|
||||||
if let largestRepresentation = largestRepresentationForPhoto(photo), let largestSize = largestRepresentation.size {
|
if let largestRepresentation = largestRepresentationForPhoto(photo) {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: largestRepresentation.location, size: largestSize)
|
return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource)
|
||||||
return account.postbox.mediaBox.cancelInteractiveResourceFetch(fullSizeResource)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> Signal<Data?, NoError> {
|
func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> Signal<Data?, NoError> {
|
||||||
if let closestRepresentation = photo.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) {
|
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(closestRepresentation.resource) |> map { next in
|
||||||
let resourceData = account.postbox.mediaBox.resourceData(resource) |> map { next in
|
|
||||||
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
|
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: {
|
}, completed: {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}))
|
}))
|
||||||
disposable.add(account.postbox.mediaBox.fetchedResource(resource).start())
|
disposable.add(account.postbox.mediaBox.fetchedResource(closestRepresentation.resource).start())
|
||||||
return disposable
|
return disposable
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -618,7 +611,7 @@ func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage) -> Sig
|
|||||||
func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
||||||
let signal = chatMessageFileDatas(account: account, file: video)
|
let signal = chatMessageFileDatas(account: account, file: video)
|
||||||
|
|
||||||
return signal |> map { (thumbnailData, fullSizeDataAndPath, fullTotalSize) in
|
return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in
|
||||||
return { arguments in
|
return { arguments in
|
||||||
assertNotOnMainThread()
|
assertNotOnMainThread()
|
||||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||||
@@ -632,7 +625,7 @@ func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(Tra
|
|||||||
|
|
||||||
var fullSizeImage: CGImage?
|
var fullSizeImage: CGImage?
|
||||||
if let fullSizeDataAndPath = fullSizeDataAndPath {
|
if let fullSizeDataAndPath = fullSizeDataAndPath {
|
||||||
if fullSizeDataAndPath.0.count >= fullTotalSize {
|
if fullSizeComplete {
|
||||||
if video.mimeType.hasPrefix("video/") {
|
if video.mimeType.hasPrefix("video/") {
|
||||||
let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov"
|
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> {
|
func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
||||||
let signal = chatMessageFileDatas(account: account, file: file, progressive: progressive)
|
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
|
return { arguments in
|
||||||
assertNotOnMainThread()
|
assertNotOnMainThread()
|
||||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||||
@@ -725,7 +718,7 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive
|
|||||||
|
|
||||||
var fullSizeImage: CGImage?
|
var fullSizeImage: CGImage?
|
||||||
if let fullSizeDataAndPath = fullSizeDataAndPath {
|
if let fullSizeDataAndPath = fullSizeDataAndPath {
|
||||||
if fullSizeDataAndPath.0.count >= fullTotalSize {
|
if fullSizeComplete {
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String)
|
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)
|
options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String)
|
||||||
@@ -734,7 +727,7 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive
|
|||||||
}
|
}
|
||||||
} else if progressive {
|
} else if progressive {
|
||||||
let imageSource = CGImageSourceCreateIncremental(nil)
|
let imageSource = CGImageSourceCreateIncremental(nil)
|
||||||
CGImageSourceUpdateData(imageSource, fullSizeDataAndPath.0 as CFData, fullSizeDataAndPath.0.count >= fullTotalSize)
|
CGImageSourceUpdateData(imageSource, fullSizeDataAndPath.0 as CFData, fullSizeComplete)
|
||||||
|
|
||||||
let options = NSMutableDictionary()
|
let options = NSMutableDictionary()
|
||||||
options[kCGImageSourceShouldCache as NSString] = false as NSNumber
|
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<MediaResourceStatus, NoError> {
|
func chatMessageFileStatus(account: Account, file: TelegramMediaFile) -> Signal<MediaResourceStatus, NoError> {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size)
|
return account.postbox.mediaBox.resourceStatus(file.resource)
|
||||||
return account.postbox.mediaBox.resourceStatus(fullSizeResource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatMessageFileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal<Void, NoError> {
|
func chatMessageFileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal<Void, NoError> {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size)
|
return account.postbox.mediaBox.fetchedResource(file.resource)
|
||||||
return account.postbox.mediaBox.fetchedResource(fullSizeResource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) {
|
func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) {
|
||||||
let fullSizeResource = CloudFileMediaResource(location: file.location, size: file.size)
|
account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource)
|
||||||
return account.postbox.mediaBox.cancelInteractiveResourceFetch(fullSizeResource)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,42 +5,71 @@ import Display
|
|||||||
import TelegramUIPrivateModule
|
import TelegramUIPrivateModule
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile) -> Signal<(Data?, Data?, Int), NoError> {
|
private func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? {
|
||||||
let fullSizeResource = fileResource(file)
|
if let (colorData, alphaData) = data.withUnsafeBytes({ (bytes: UnsafePointer<UInt8>) -> (Data, Data)? in
|
||||||
let maybeFetched = account.postbox.mediaBox.resourceData(fullSizeResource, complete: true)
|
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
|
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: [])
|
let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: [])
|
||||||
|
|
||||||
return .single((nil, loadedData, fullSizeResource.size))
|
return .single((nil, loadedData, true))
|
||||||
} else {
|
} else {
|
||||||
let fullSizeData = account.postbox.mediaBox.resourceData(fullSizeResource, complete: true) |> map { next in
|
//let fullSizeData = account.postbox.mediaBox.resourceData(file.resource, complete: true)
|
||||||
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
|
|
||||||
|
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 fullSizeData |> map { (data, complete) -> (Data?, Data?, Bool) in
|
||||||
return (nil, data, fullSizeResource.size)
|
return (nil, data, complete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatMessageSticker(account: Account, file: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool) -> Signal<(TransformImageArguments) -> DrawingContext, NoError> {
|
||||||
let signal = chatMessageStickerDatas(account: account, file: file)
|
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
|
return { arguments in
|
||||||
assertNotOnMainThread()
|
assertNotOnMainThread()
|
||||||
|
|
||||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||||
|
|
||||||
var fullSizeImage: UIImage?
|
var fullSizeImage: (UIImage, UIImage)?
|
||||||
if let fullSizeData = fullSizeData {
|
if let fullSizeData = fullSizeData, fullSizeComplete {
|
||||||
if fullSizeData.count >= fullTotalSize {
|
if let image = imageFromAJpeg(data: fullSizeData) {
|
||||||
if let image = UIImage.convert(fromWebP: fullSizeData) {
|
fullSizeImage = image
|
||||||
fullSizeImage = image
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +96,13 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile) -> Signal<(Tr
|
|||||||
c.draw(blurredThumbnailImage.cgImage!, in: arguments.drawingRect)
|
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.setBlendMode(.normal)
|
||||||
c.interpolationQuality = .medium
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ private enum UserInfoSection: UInt32, PeerInfoSection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DestructiveUserInfoAction {
|
||||||
|
case block
|
||||||
|
case removeContact
|
||||||
|
}
|
||||||
|
|
||||||
enum UserInfoEntry: PeerInfoEntry {
|
enum UserInfoEntry: PeerInfoEntry {
|
||||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
case info(peer: Peer?, cachedData: CachedPeerData?, editingState: PeerInfoAvatarAndNameItemEditingState?)
|
||||||
case about(text: String)
|
case about(text: String)
|
||||||
case phoneNumber(index: Int, value: PhoneNumberWithLabel)
|
case phoneNumber(index: Int, value: PhoneNumberWithLabel)
|
||||||
case userName(value: String)
|
case userName(value: String)
|
||||||
@@ -35,7 +40,8 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
case startSecretChat
|
case startSecretChat
|
||||||
case sharedMedia
|
case sharedMedia
|
||||||
case notifications(settings: PeerNotificationSettings?)
|
case notifications(settings: PeerNotificationSettings?)
|
||||||
case block
|
case notificationSound(settings: PeerNotificationSettings?)
|
||||||
|
case block(action: DestructiveUserInfoAction)
|
||||||
|
|
||||||
var section: PeerInfoSection {
|
var section: PeerInfoSection {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -43,7 +49,7 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
return UserInfoSection.info
|
return UserInfoSection.info
|
||||||
case .sendMessage, .shareContact, .startSecretChat:
|
case .sendMessage, .shareContact, .startSecretChat:
|
||||||
return UserInfoSection.actions
|
return UserInfoSection.actions
|
||||||
case .sharedMedia, .notifications:
|
case .sharedMedia, .notifications, .notificationSound:
|
||||||
return UserInfoSection.sharedMediaAndNotifications
|
return UserInfoSection.sharedMediaAndNotifications
|
||||||
case .block:
|
case .block:
|
||||||
return UserInfoSection.block
|
return UserInfoSection.block
|
||||||
@@ -60,9 +66,9 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case let .info(lhsPeer, lhsCachedData):
|
case let .info(lhsPeer, lhsCachedData, lhsEditingState):
|
||||||
switch entry {
|
switch entry {
|
||||||
case let .info(rhsPeer, rhsCachedData):
|
case let .info(rhsPeer, rhsCachedData, rhsEditingState):
|
||||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||||
if !lhsPeer.isEqual(rhsPeer) {
|
if !lhsPeer.isEqual(rhsPeer) {
|
||||||
return false
|
return false
|
||||||
@@ -77,6 +83,9 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
|
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhsEditingState != rhsEditingState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -142,9 +151,21 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case .block:
|
case let .notificationSound(lhsSettings):
|
||||||
switch entry {
|
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
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -172,8 +193,10 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
return 1004
|
return 1004
|
||||||
case .notifications:
|
case .notifications:
|
||||||
return 1005
|
return 1005
|
||||||
case .block:
|
case .notificationSound:
|
||||||
return 1006
|
return 1006
|
||||||
|
case .block:
|
||||||
|
return 1007
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +210,8 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
|
|
||||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||||
switch self {
|
switch self {
|
||||||
case let .info(peer, cachedData):
|
case let .info(peer, cachedData, editingState):
|
||||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain)
|
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, editingState: editingState, sectionId: self.section.rawValue, style: .plain)
|
||||||
case let .about(text):
|
case let .about(text):
|
||||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
||||||
case let .phoneNumber(_, value):
|
case let .phoneNumber(_, value):
|
||||||
@@ -219,65 +242,141 @@ enum UserInfoEntry: PeerInfoEntry {
|
|||||||
label = "Enabled"
|
label = "Enabled"
|
||||||
}
|
}
|
||||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
||||||
interaction.changeNotificationNoteSettings()
|
interaction.changeNotificationMuteSettings()
|
||||||
})
|
})
|
||||||
case .block:
|
case let .notificationSound(settings):
|
||||||
return PeerInfoActionItem(title: "Block User", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
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?
|
fileprivate let editingState: UserInfoEditingState?
|
||||||
|
|
||||||
var navigationButton: PeerInfoNavigationButton {
|
init(editingState: UserInfoEditingState?) {
|
||||||
return self.editingState == nil ? .edit : .done
|
self.editingState = editingState
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.editingState = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEqual(to: PeerInfoState) -> Bool {
|
func isEqual(to: PeerInfoState) -> Bool {
|
||||||
if let to = to as? UserInfoState {
|
if let to = to as? UserInfoState {
|
||||||
return true
|
return self.editingState == to.editingState
|
||||||
} else {
|
} else {
|
||||||
return false
|
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] = []
|
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 cachedUserData = view.cachedData as? CachedUserData {
|
||||||
if let about = cachedUserData.about, !about.isEmpty {
|
if let about = cachedUserData.about, !about.isEmpty {
|
||||||
entries.append(UserInfoEntry.about(text: about))
|
entries.append(UserInfoEntry.about(text: about))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var editable = true
|
||||||
|
|
||||||
if let user = view.peers[view.peerId] as? TelegramUser {
|
if let user = view.peers[view.peerId] as? TelegramUser {
|
||||||
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
|
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
|
||||||
entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber)))
|
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 !isEditing {
|
||||||
}
|
if let username = user.username, !username.isEmpty {
|
||||||
if let state = state as? UserInfoState, let editingState = state.editingState {
|
entries.append(UserInfoEntry.userName(value: username))
|
||||||
|
}
|
||||||
} else {
|
|
||||||
entries.append(UserInfoEntry.sendMessage)
|
entries.append(UserInfoEntry.sendMessage)
|
||||||
entries.append(UserInfoEntry.shareContact)
|
if view.peerIsContact {
|
||||||
|
entries.append(UserInfoEntry.shareContact)
|
||||||
|
}
|
||||||
entries.append(UserInfoEntry.startSecretChat)
|
entries.append(UserInfoEntry.startSecretChat)
|
||||||
|
entries.append(UserInfoEntry.sharedMedia)
|
||||||
}
|
}
|
||||||
entries.append(UserInfoEntry.sharedMedia)
|
|
||||||
entries.append(UserInfoEntry.notifications(settings: view.notificationSettings))
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user