no message

This commit is contained in:
Peter
2016-10-22 22:53:47 +03:00
parent 702d19a07d
commit 6fbab164ed
70 changed files with 2188 additions and 520 deletions

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_bot.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_keyboard.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_sticker.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -2,7 +2,7 @@
"images" : [ "images" : [
{ {
"idiom" : "universal", "idiom" : "universal",
"filename" : "IconAttachment.pdf" "filename" : "ic_attach.pdf"
} }
], ],
"info" : { "info" : {

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_voice.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Send.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View File

@@ -6,7 +6,7 @@
}, },
{ {
"idiom" : "universal", "idiom" : "universal",
"filename" : "builtin-wallpaper-0.jpg", "filename" : "Dogs BG.jpg",
"scale" : "2x" "scale" : "2x"
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -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 */,

View File

@@ -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 {

View File

@@ -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
}
}

View 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
}
}
}

View File

@@ -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)
} }

View File

@@ -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
}
} }

View File

@@ -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: [

View File

@@ -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
} }
} }

View File

@@ -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
}*/
}
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
} }

View 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
}
}

View File

@@ -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
} }
} }

View File

@@ -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: [])
}
}
}

View 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
}

View File

@@ -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
} }
} }

View File

@@ -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()

View File

@@ -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)
}
}))
} }
} }

View 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: { })
}
}

View 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 })
}
}

View 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: {
})
}
}
}

View 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)
}*/
}
}

View 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
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
} }

View File

@@ -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())
} }

View File

@@ -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 {

View File

@@ -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
} }
} }

View File

@@ -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))
} }
} }

View File

@@ -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)
}*/
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View 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)
}

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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()
} }

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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)
} }
} }

View File

@@ -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
}
}
} }
}) })
} }

View File

@@ -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))
}
}
} }

View File

@@ -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)
} }
} }

View File

@@ -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)
} }

View File

@@ -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)
} }
} }

View File

@@ -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)
} }
} }

View File

@@ -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)
} }
} }

View File

@@ -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 })

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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)
} }
} }

View File

@@ -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)
} }