no message

This commit is contained in:
Peter
2016-08-23 16:21:34 +03:00
parent d957d51b7c
commit e641db56e9
275 changed files with 42488 additions and 7 deletions

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconMessages@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconMessages_Highlighted@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconContacts@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconContacts_Highlighted@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconSettings@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "TabIconSettings_Highlighted@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 3,
"height" : 1
},
"cap-insets" : {
"bottom" : 32,
"top" : 32,
"right" : 33,
"left" : 41
}
},
"idiom" : "universal",
"filename" : "ModernBubbleIncomingFullPad@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 33,
"top" : 32,
"right" : 34,
"left" : 44
}
},
"idiom" : "universal",
"filename" : "ModernBubbleIncomingPartialPad@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 32,
"top" : 33,
"right" : 49,
"left" : 30
}
},
"idiom" : "universal",
"filename" : "ModernBubbleIncomingMergedBoth@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 32,
"top" : 33,
"right" : 35,
"left" : 44
}
},
"idiom" : "universal",
"filename" : "ModernBubbleIncomingMergedBottom@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 33,
"top" : 32,
"right" : 36,
"left" : 43
}
},
"idiom" : "universal",
"filename" : "ModernBubbleIncomingMergedTop@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 33,
"top" : 31,
"right" : 43,
"left" : 34
}
},
"idiom" : "universal",
"filename" : "ModernBubbleOutgoingFullPad@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"resizing" : {
"mode" : "9-part",
"center" : {
"mode" : "stretch",
"width" : 1,
"height" : 1
},
"cap-insets" : {
"bottom" : 33,
"top" : 32,
"right" : 44,
"left" : 34
}
},
"idiom" : "universal",
"filename" : "ModernBubbleOutgoingPartialPad@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ModernMessageDocumentIconIncoming@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ModernMessageDocumentIconIncoming@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ModernMessageDocumentIconOutgoing@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ModernMessageDocumentIconOutgoing@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "builtin-wallpaper-0.jpg",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -14,9 +14,136 @@
D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */; };
D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB21D6718EB002C78E7 /* libz.tbd */; };
D0AB0BB51D6718F1002C78E7 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */; };
D0AB0BB81D67191C002C78E7 /* MtProtoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB61D67191C002C78E7 /* MtProtoKit.framework */; };
D0AB0BB91D67191C002C78E7 /* SSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB71D67191C002C78E7 /* SSignalKit.framework */; };
D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; };
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; };
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; };
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */; };
D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD71D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift */; };
D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CDC1D6B87D30046BCD6 /* MediaPlayerNode.swift */; };
D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */; };
D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CE11D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift */; };
D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CE51D6B87D30046BCD6 /* MediaFrameSource.swift */; };
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CFB1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift */; };
D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D021D6B87D30046BCD6 /* MediaPlayer.swift */; };
D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D161D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift */; };
D0F69D671D6B87D30046BCD6 /* FFMpegPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D171D6B87D30046BCD6 /* FFMpegPacket.swift */; };
D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D1D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift */; };
D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D6F1D6B87DE0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */; };
D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D701D6B87DE0046BCD6 /* MediaTrackFrameBuffer.swift */; };
D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D711D6B87DE0046BCD6 /* MediaTrackFrame.swift */; };
D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */; };
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */; };
D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */; };
D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69D901D6B87EC0046BCD6 /* Cache.swift */; };
D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; };
D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */; };
D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */; };
D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */; };
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC61D6B89E70046BCD6 /* TransformImageNode.swift */; };
D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DC81D6B89EB0046BCD6 /* ImageNode.swift */; };
D0F69DCF1D6B8A0D0046BCD6 /* SearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DCB1D6B8A0D0046BCD6 /* SearchBarNode.swift */; };
D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DCC1D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift */; };
D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DCD1D6B8A0D0046BCD6 /* SearchDisplayController.swift */; };
D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DCE1D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift */; };
D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DD51D6B8A2D0046BCD6 /* AlertController.swift */; };
D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DD81D6B8A420046BCD6 /* ListController.swift */; };
D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DD91D6B8A420046BCD6 /* ListControllerButtonItem.swift */; };
D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDA1D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift */; };
D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDB1D6B8A420046BCD6 /* ListControllerGroupableItem.swift */; };
D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */; };
D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */; };
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */; };
D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */; };
D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */; };
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */; };
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */; };
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */; };
D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */; };
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */; };
D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */; };
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; };
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; };
D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */; };
D0F69E021D6B8A880046BCD6 /* ChatListHoleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */; };
D0F69E031D6B8A880046BCD6 /* ChatListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */; };
D0F69E041D6B8A880046BCD6 /* ChatListSearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */; };
D0F69E081D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E071D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift */; };
D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E091D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift */; };
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E0B1D6B8AB10046BCD6 /* HorizontalPeerItem.swift */; };
D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E0E1D6B8ACF0046BCD6 /* ChatController.swift */; };
D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E0F1D6B8ACF0046BCD6 /* ChatControllerInteraction.swift */; };
D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E101D6B8ACF0046BCD6 /* ChatControllerNode.swift */; };
D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */; };
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */; };
D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */; };
D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E1B1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift */; };
D0F69E2E1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E1C1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift */; };
D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E1D1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift */; };
D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E1E1D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift */; };
D0F69E311D6B8B030046BCD6 /* ChatMessageBubbleItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E1F1D6B8B030046BCD6 /* ChatMessageBubbleItemNode.swift */; };
D0F69E321D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E201D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift */; };
D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E211D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift */; };
D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E221D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift */; };
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E231D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift */; };
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E241D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift */; };
D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E251D6B8B030046BCD6 /* ChatMessageItem.swift */; };
D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E261D6B8B030046BCD6 /* ChatMessageItemView.swift */; };
D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E271D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift */; };
D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E281D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift */; };
D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E291D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift */; };
D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */; };
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */; };
D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */; };
D0F69E421D6B8B7E0046BCD6 /* ChatInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */; };
D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */; };
D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */; };
D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */; };
D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E4A1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift */; };
D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E4B1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift */; };
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E501D6B8BDA0046BCD6 /* GalleryController.swift */; };
D0F69E561D6B8BDA0046BCD6 /* GalleryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E511D6B8BDA0046BCD6 /* GalleryControllerNode.swift */; };
D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E521D6B8BDA0046BCD6 /* GalleryItem.swift */; };
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E531D6B8BDA0046BCD6 /* GalleryItemNode.swift */; };
D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E541D6B8BDA0046BCD6 /* GalleryPagerNode.swift */; };
D0F69E611D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */; };
D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */; };
D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */; };
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */; };
D0F69E651D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */; };
D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */; };
D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E681D6B8C160046BCD6 /* MapInputController.swift */; };
D0F69E6B1D6B8C160046BCD6 /* MapInputControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E691D6B8C160046BCD6 /* MapInputControllerNode.swift */; };
D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E6D1D6B8C340046BCD6 /* ContactsController.swift */; };
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E6E1D6B8C340046BCD6 /* ContactsControllerNode.swift */; };
D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */; };
D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E701D6B8C340046BCD6 /* ContactsSearchContainerNode.swift */; };
D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */; };
D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */; };
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */; };
D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */; };
D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */; };
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E801D6B8C850046BCD6 /* FastBlur.m */; };
D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */; };
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E821D6B8C850046BCD6 /* FFMpegSwResample.m */; };
D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E831D6B8C850046BCD6 /* FrameworkBundle.swift */; };
D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E841D6B8C850046BCD6 /* Localizable.swift */; };
D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E851D6B8C850046BCD6 /* RingBuffer.h */; };
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E861D6B8C850046BCD6 /* RingBuffer.m */; };
D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */; };
D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */; };
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */; };
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E941D6B8C9B0046BCD6 /* WebP.swift */; };
D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */; };
D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E991D6B8D200046BCD6 /* UIImage+WebP.m */; };
D0F69E9C1D6B8D520046BCD6 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452D1D5E340300A7428A /* TelegramCore.framework */; };
D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */; };
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */; };
D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */; };
D0F69EA71D6B9BBC0046BCD6 /* libwebp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EA61D6B9BBC0046BCD6 /* libwebp.a */; };
D0F69EAC1D6B9BCB0046BCD6 /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EA81D6B9BCB0046BCD6 /* libavcodec.a */; };
D0F69EAD1D6B9BCB0046BCD6 /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */; };
D0F69EAE1D6B9BCB0046BCD6 /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */; };
D0F69EAF1D6B9BCB0046BCD6 /* libswresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */; };
D0FC40891D5B8E7500261D9D /* TelegramUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0FC407F1D5B8E7400261D9D /* TelegramUI.framework */; };
D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; };
D0FC40901D5B8E7500261D9D /* TelegramUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC40821D5B8E7400261D9D /* TelegramUI.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -44,6 +171,135 @@
D0AB0BB61D67191C002C78E7 /* MtProtoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MtProtoKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/MtProtoKit.framework"; sourceTree = "<group>"; };
D0AB0BB71D67191C002C78E7 /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/SSignalKit.framework"; sourceTree = "<group>"; };
D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = "<group>"; };
D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = "<group>"; };
D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = "<group>"; };
D0F69CD71D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegAudioFrameDecoder.swift; sourceTree = "<group>"; };
D0F69CDC1D6B87D30046BCD6 /* MediaPlayerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerNode.swift; sourceTree = "<group>"; };
D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAvatar.swift; sourceTree = "<group>"; };
D0F69CE11D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSource.swift; sourceTree = "<group>"; };
D0F69CE51D6B87D30046BCD6 /* MediaFrameSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFrameSource.swift; sourceTree = "<group>"; };
D0F69CFB1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchDownGestureRecognizer.swift; sourceTree = "<group>"; };
D0F69D021D6B87D30046BCD6 /* MediaPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayer.swift; sourceTree = "<group>"; };
D0F69D161D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContextHelpers.swift; sourceTree = "<group>"; };
D0F69D171D6B87D30046BCD6 /* FFMpegPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegPacket.swift; sourceTree = "<group>"; };
D0F69D1D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackDecodableFrame.swift; sourceTree = "<group>"; };
D0F69D6F1D6B87DE0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaPassthroughVideoFrameDecoder.swift; sourceTree = "<group>"; };
D0F69D701D6B87DE0046BCD6 /* MediaTrackFrameBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackFrameBuffer.swift; sourceTree = "<group>"; };
D0F69D711D6B87DE0046BCD6 /* MediaTrackFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTrackFrame.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>"; };
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>"; };
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>"; };
D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialProgressNode.swift; sourceTree = "<group>"; };
D0F69DC61D6B89E70046BCD6 /* TransformImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformImageNode.swift; sourceTree = "<group>"; };
D0F69DC81D6B89EB0046BCD6 /* ImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageNode.swift; sourceTree = "<group>"; };
D0F69DCB1D6B8A0D0046BCD6 /* SearchBarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarNode.swift; sourceTree = "<group>"; };
D0F69DCC1D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarPlaceholderNode.swift; sourceTree = "<group>"; };
D0F69DCD1D6B8A0D0046BCD6 /* SearchDisplayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchDisplayController.swift; sourceTree = "<group>"; };
D0F69DCE1D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchDisplayControllerContentNode.swift; sourceTree = "<group>"; };
D0F69DD51D6B8A2D0046BCD6 /* AlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertController.swift; sourceTree = "<group>"; };
D0F69DD81D6B8A420046BCD6 /* ListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListController.swift; sourceTree = "<group>"; };
D0F69DD91D6B8A420046BCD6 /* ListControllerButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerButtonItem.swift; sourceTree = "<group>"; };
D0F69DDA1D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerDisclosureActionItem.swift; sourceTree = "<group>"; };
D0F69DDB1D6B8A420046BCD6 /* ListControllerGroupableItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerGroupableItem.swift; sourceTree = "<group>"; };
D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerItem.swift; sourceTree = "<group>"; };
D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerNode.swift; sourceTree = "<group>"; };
D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerSpacerItem.swift; sourceTree = "<group>"; };
D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeController.swift; sourceTree = "<group>"; };
D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeControllerNode.swift; sourceTree = "<group>"; };
D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationController.swift; sourceTree = "<group>"; };
D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordController.swift; sourceTree = "<group>"; };
D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = "<group>"; };
D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = "<group>"; };
D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = "<group>"; };
D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListAvatarNode.swift; sourceTree = "<group>"; };
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; };
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = "<group>"; };
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListEmptyItem.swift; sourceTree = "<group>"; };
D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListHoleItem.swift; sourceTree = "<group>"; };
D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListItem.swift; sourceTree = "<group>"; };
D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListSearchItem.swift; sourceTree = "<group>"; };
D0F69E071D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListSearchContainerNode.swift; sourceTree = "<group>"; };
D0F69E091D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListSearchRecentPeersNode.swift; sourceTree = "<group>"; };
D0F69E0B1D6B8AB10046BCD6 /* HorizontalPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalPeerItem.swift; sourceTree = "<group>"; };
D0F69E0E1D6B8ACF0046BCD6 /* ChatController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatController.swift; sourceTree = "<group>"; };
D0F69E0F1D6B8ACF0046BCD6 /* ChatControllerInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatControllerInteraction.swift; sourceTree = "<group>"; };
D0F69E101D6B8ACF0046BCD6 /* ChatControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatControllerNode.swift; sourceTree = "<group>"; };
D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntry.swift; sourceTree = "<group>"; };
D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryLocation.swift; sourceTree = "<group>"; };
D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHoleItem.swift; sourceTree = "<group>"; };
D0F69E1B1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionItemNode.swift; sourceTree = "<group>"; };
D0F69E1C1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageAvatarAccessoryItem.swift; sourceTree = "<group>"; };
D0F69E1D1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleContentCalclulateImageCorners.swift; sourceTree = "<group>"; };
D0F69E1E1D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleContentNode.swift; sourceTree = "<group>"; };
D0F69E1F1D6B8B030046BCD6 /* ChatMessageBubbleItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleItemNode.swift; sourceTree = "<group>"; };
D0F69E201D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDateAndStatusNode.swift; sourceTree = "<group>"; };
D0F69E211D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageFileBubbleContentNode.swift; sourceTree = "<group>"; };
D0F69E221D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageForwardInfoNode.swift; sourceTree = "<group>"; };
D0F69E231D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveFileNode.swift; sourceTree = "<group>"; };
D0F69E241D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveMediaNode.swift; sourceTree = "<group>"; };
D0F69E251D6B8B030046BCD6 /* ChatMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageItem.swift; sourceTree = "<group>"; };
D0F69E261D6B8B030046BCD6 /* ChatMessageItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageItemView.swift; sourceTree = "<group>"; };
D0F69E271D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageMediaBubbleContentNode.swift; sourceTree = "<group>"; };
D0F69E281D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageReplyInfoNode.swift; sourceTree = "<group>"; };
D0F69E291D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageStickerItemNode.swift; sourceTree = "<group>"; };
D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageTextBubbleContentNode.swift; sourceTree = "<group>"; };
D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageWebpageBubbleContentNode.swift; sourceTree = "<group>"; };
D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnreadItem.swift; sourceTree = "<group>"; };
D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = "<group>"; };
D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResizeableTextInputView.swift; sourceTree = "<group>"; };
D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtonNode.swift; sourceTree = "<group>"; };
D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetRollImageItem.swift; sourceTree = "<group>"; };
D0F69E4A1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaActionSheetController.swift; sourceTree = "<group>"; };
D0F69E4B1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaActionSheetRollItem.swift; sourceTree = "<group>"; };
D0F69E501D6B8BDA0046BCD6 /* GalleryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryController.swift; sourceTree = "<group>"; };
D0F69E511D6B8BDA0046BCD6 /* GalleryControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryControllerNode.swift; sourceTree = "<group>"; };
D0F69E521D6B8BDA0046BCD6 /* GalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryItem.swift; sourceTree = "<group>"; };
D0F69E531D6B8BDA0046BCD6 /* GalleryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryItemNode.swift; sourceTree = "<group>"; };
D0F69E541D6B8BDA0046BCD6 /* GalleryPagerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryPagerNode.swift; sourceTree = "<group>"; };
D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatDocumentGalleryItem.swift; sourceTree = "<group>"; };
D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHoleGalleryItem.swift; sourceTree = "<group>"; };
D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatImageGalleryItem.swift; sourceTree = "<group>"; };
D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoGalleryItem.swift; sourceTree = "<group>"; };
D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoGalleryItemScrubberView.swift; sourceTree = "<group>"; };
D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomableContentGalleryItemNode.swift; sourceTree = "<group>"; };
D0F69E681D6B8C160046BCD6 /* MapInputController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapInputController.swift; sourceTree = "<group>"; };
D0F69E691D6B8C160046BCD6 /* MapInputControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapInputControllerNode.swift; sourceTree = "<group>"; };
D0F69E6D1D6B8C340046BCD6 /* ContactsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsController.swift; sourceTree = "<group>"; };
D0F69E6E1D6B8C340046BCD6 /* ContactsControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsControllerNode.swift; sourceTree = "<group>"; };
D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsPeerItem.swift; sourceTree = "<group>"; };
D0F69E701D6B8C340046BCD6 /* ContactsSearchContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSearchContainerNode.swift; sourceTree = "<group>"; };
D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSectionHeaderAccessoryItem.swift; sourceTree = "<group>"; };
D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsVCardItem.swift; sourceTree = "<group>"; };
D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAccountInfoItem.swift; sourceTree = "<group>"; };
D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = "<group>"; };
D0F69E801D6B8C850046BCD6 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = "<group>"; };
D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSwResample.h; sourceTree = "<group>"; };
D0F69E821D6B8C850046BCD6 /* FFMpegSwResample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegSwResample.m; sourceTree = "<group>"; };
D0F69E831D6B8C850046BCD6 /* FrameworkBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameworkBundle.swift; sourceTree = "<group>"; };
D0F69E841D6B8C850046BCD6 /* Localizable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localizable.swift; sourceTree = "<group>"; };
D0F69E851D6B8C850046BCD6 /* RingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingBuffer.h; sourceTree = "<group>"; };
D0F69E861D6B8C850046BCD6 /* RingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingBuffer.m; sourceTree = "<group>"; };
D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingByteBuffer.swift; sourceTree = "<group>"; };
D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRepresentationsUtils.swift; sourceTree = "<group>"; };
D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressiveImage.swift; sourceTree = "<group>"; };
D0F69E941D6B8C9B0046BCD6 /* WebP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebP.swift; sourceTree = "<group>"; };
D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+WebP.h"; sourceTree = "<group>"; };
D0F69E991D6B8D200046BCD6 /* UIImage+WebP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+WebP.m"; sourceTree = "<group>"; };
D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileResources.swift; sourceTree = "<group>"; };
D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoResources.swift; sourceTree = "<group>"; };
D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerResources.swift; sourceTree = "<group>"; };
D0F69EA51D6B8F3E0046BCD6 /* TelegramUIIncludes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramUIIncludes.h; sourceTree = "<group>"; };
D0F69EA61D6B9BBC0046BCD6 /* libwebp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebp.a; path = "third-party/libwebp/lib/libwebp.a"; sourceTree = "<group>"; };
D0F69EA81D6B9BCB0046BCD6 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = "third-party/FFmpeg-iOS/lib/libavcodec.a"; sourceTree = "<group>"; };
D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "third-party/FFmpeg-iOS/lib/libavformat.a"; sourceTree = "<group>"; };
D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "third-party/FFmpeg-iOS/lib/libavutil.a"; sourceTree = "<group>"; };
D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = "<group>"; };
D0FC407F1D5B8E7400261D9D /* TelegramUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TelegramUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D0FC40821D5B8E7400261D9D /* TelegramUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramUI.h; sourceTree = "<group>"; };
D0FC40831D5B8E7400261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -57,8 +313,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D0AB0BB81D67191C002C78E7 /* MtProtoKit.framework in Frameworks */,
D0AB0BB91D67191C002C78E7 /* SSignalKit.framework in Frameworks */,
D0F69EAC1D6B9BCB0046BCD6 /* libavcodec.a in Frameworks */,
D0F69EAD1D6B9BCB0046BCD6 /* libavformat.a in Frameworks */,
D0F69EAE1D6B9BCB0046BCD6 /* libavutil.a in Frameworks */,
D0F69EAF1D6B9BCB0046BCD6 /* libswresample.a in Frameworks */,
D0F69EA71D6B9BBC0046BCD6 /* libwebp.a in Frameworks */,
D0F69E9C1D6B8D520046BCD6 /* TelegramCore.framework in Frameworks */,
D0AB0BB51D6718F1002C78E7 /* CoreMedia.framework in Frameworks */,
D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */,
D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */,
@@ -83,6 +343,11 @@
D08D45281D5E340200A7428A /* Frameworks */ = {
isa = PBXGroup;
children = (
D0F69EA81D6B9BCB0046BCD6 /* libavcodec.a */,
D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */,
D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */,
D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */,
D0F69EA61D6B9BBC0046BCD6 /* libwebp.a */,
D0AB0BB61D67191C002C78E7 /* MtProtoKit.framework */,
D0AB0BB71D67191C002C78E7 /* SSignalKit.framework */,
D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */,
@@ -97,9 +362,359 @@
name = Frameworks;
sourceTree = "<group>";
};
D0F69CCE1D6B87950046BCD6 /* Files */ = {
isa = PBXGroup;
children = (
);
name = Files;
sourceTree = "<group>";
};
D0F69DBB1D6B88330046BCD6 /* Media */ = {
isa = PBXGroup;
children = (
D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */,
D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */,
D0F69D901D6B87EC0046BCD6 /* Cache.swift */,
D0F69DBC1D6B886C0046BCD6 /* Player */,
D0F69E9D1D6B8E240046BCD6 /* Resources */,
);
name = Media;
sourceTree = "<group>";
};
D0F69DBC1D6B886C0046BCD6 /* Player */ = {
isa = PBXGroup;
children = (
D0F69CE51D6B87D30046BCD6 /* MediaFrameSource.swift */,
D0F69D7F1D6B87EC0046BCD6 /* MediaPlaybackData.swift */,
D0F69D021D6B87D30046BCD6 /* MediaPlayer.swift */,
D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */,
D0F69CDC1D6B87D30046BCD6 /* MediaPlayerNode.swift */,
D0F69D1D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift */,
D0F69D711D6B87DE0046BCD6 /* MediaTrackFrame.swift */,
D0F69D701D6B87DE0046BCD6 /* MediaTrackFrameBuffer.swift */,
D0F69D881D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift */,
D0F69CD71D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift */,
D0F69CE11D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift */,
D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */,
D0F69D161D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift */,
D0F69D871D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift */,
D0F69D6F1D6B87DE0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */,
D0F69D171D6B87D30046BCD6 /* FFMpegPacket.swift */,
);
name = Player;
sourceTree = "<group>";
};
D0F69DBD1D6B897A0046BCD6 /* Components */ = {
isa = PBXGroup;
children = (
D0F69DBE1D6B89880046BCD6 /* Gestures */,
D0F69DBF1D6B89AE0046BCD6 /* Nodes */,
D0F69DD31D6B8A160046BCD6 /* Controllers */,
);
name = Components;
sourceTree = "<group>";
};
D0F69DBE1D6B89880046BCD6 /* Gestures */ = {
isa = PBXGroup;
children = (
D0F69CFB1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift */,
);
name = Gestures;
sourceTree = "<group>";
};
D0F69DBF1D6B89AE0046BCD6 /* Nodes */ = {
isa = PBXGroup;
children = (
D0F69DC81D6B89EB0046BCD6 /* ImageNode.swift */,
D0F69DC61D6B89E70046BCD6 /* TransformImageNode.swift */,
D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */,
D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */,
D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */,
D0F69DCA1D6B89F20046BCD6 /* Search */,
);
name = Nodes;
sourceTree = "<group>";
};
D0F69DCA1D6B89F20046BCD6 /* Search */ = {
isa = PBXGroup;
children = (
D0F69DCB1D6B8A0D0046BCD6 /* SearchBarNode.swift */,
D0F69DCC1D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift */,
D0F69DCD1D6B8A0D0046BCD6 /* SearchDisplayController.swift */,
D0F69DCE1D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift */,
);
name = Search;
sourceTree = "<group>";
};
D0F69DD31D6B8A160046BCD6 /* Controllers */ = {
isa = PBXGroup;
children = (
D0F69DD71D6B8A300046BCD6 /* List */,
D0F69DD41D6B8A240046BCD6 /* Alert */,
);
name = Controllers;
sourceTree = "<group>";
};
D0F69DD41D6B8A240046BCD6 /* Alert */ = {
isa = PBXGroup;
children = (
D0F69DD51D6B8A2D0046BCD6 /* AlertController.swift */,
);
name = Alert;
sourceTree = "<group>";
};
D0F69DD71D6B8A300046BCD6 /* List */ = {
isa = PBXGroup;
children = (
D0F69DD81D6B8A420046BCD6 /* ListController.swift */,
D0F69DD91D6B8A420046BCD6 /* ListControllerButtonItem.swift */,
D0F69DDA1D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift */,
D0F69DDB1D6B8A420046BCD6 /* ListControllerGroupableItem.swift */,
D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */,
D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */,
D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */,
);
name = List;
sourceTree = "<group>";
};
D0F69DE61D6B8A4E0046BCD6 /* Controllers */ = {
isa = PBXGroup;
children = (
D0F69DE71D6B8A590046BCD6 /* Authorization */,
D0F69DF61D6B8A720046BCD6 /* Chat List */,
D0F69E0D1D6B8AB90046BCD6 /* Chat */,
D0F69E4E1D6B8BB90046BCD6 /* Media */,
D0F69E6C1D6B8C220046BCD6 /* Contacts */,
D0F69E791D6B8C3B0046BCD6 /* Settings */,
);
name = Controllers;
sourceTree = "<group>";
};
D0F69DE71D6B8A590046BCD6 /* Authorization */ = {
isa = PBXGroup;
children = (
D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */,
D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */,
D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */,
D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */,
D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */,
D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */,
D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */,
);
name = Authorization;
sourceTree = "<group>";
};
D0F69DF61D6B8A720046BCD6 /* Chat List */ = {
isa = PBXGroup;
children = (
D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */,
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */,
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */,
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */,
D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */,
D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */,
D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */,
D0F69E051D6B8A8B0046BCD6 /* Search */,
);
name = "Chat List";
sourceTree = "<group>";
};
D0F69E051D6B8A8B0046BCD6 /* Search */ = {
isa = PBXGroup;
children = (
D0F69E071D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift */,
D0F69E061D6B8A930046BCD6 /* Recent Peers */,
);
name = Search;
sourceTree = "<group>";
};
D0F69E061D6B8A930046BCD6 /* Recent Peers */ = {
isa = PBXGroup;
children = (
D0F69E0B1D6B8AB10046BCD6 /* HorizontalPeerItem.swift */,
D0F69E091D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift */,
);
name = "Recent Peers";
sourceTree = "<group>";
};
D0F69E0D1D6B8AB90046BCD6 /* Chat */ = {
isa = PBXGroup;
children = (
D0F69E0E1D6B8ACF0046BCD6 /* ChatController.swift */,
D0F69E0F1D6B8ACF0046BCD6 /* ChatControllerInteraction.swift */,
D0F69E101D6B8ACF0046BCD6 /* ChatControllerNode.swift */,
D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */,
D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */,
D0F69E181D6B8AD10046BCD6 /* Items */,
D0F69E3F1D6B8B6B0046BCD6 /* Input Panel */,
D0F69E441D6B8B850046BCD6 /* History Navigation */,
D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */,
);
name = Chat;
sourceTree = "<group>";
};
D0F69E181D6B8AD10046BCD6 /* Items */ = {
isa = PBXGroup;
children = (
D0F69E1B1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift */,
D0F69E1C1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift */,
D0F69E1D1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift */,
D0F69E1E1D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift */,
D0F69E1F1D6B8B030046BCD6 /* ChatMessageBubbleItemNode.swift */,
D0F69E201D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift */,
D0F69E211D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift */,
D0F69E221D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift */,
D0F69E231D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift */,
D0F69E241D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift */,
D0F69E251D6B8B030046BCD6 /* ChatMessageItem.swift */,
D0F69E261D6B8B030046BCD6 /* ChatMessageItemView.swift */,
D0F69E271D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift */,
D0F69E281D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift */,
D0F69E291D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift */,
D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */,
D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */,
D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */,
D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */,
);
name = Items;
sourceTree = "<group>";
};
D0F69E3F1D6B8B6B0046BCD6 /* Input Panel */ = {
isa = PBXGroup;
children = (
D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */,
D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */,
);
name = "Input Panel";
sourceTree = "<group>";
};
D0F69E441D6B8B850046BCD6 /* History Navigation */ = {
isa = PBXGroup;
children = (
D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */,
);
name = "History Navigation";
sourceTree = "<group>";
};
D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */ = {
isa = PBXGroup;
children = (
D0F69E4A1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift */,
D0F69E4B1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift */,
D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */,
);
name = "Input Media Action Sheet";
sourceTree = "<group>";
};
D0F69E4E1D6B8BB90046BCD6 /* Media */ = {
isa = PBXGroup;
children = (
D0F69E4F1D6B8BC40046BCD6 /* Gallery */,
D0F69E671D6B8C030046BCD6 /* Map Input */,
);
name = Media;
sourceTree = "<group>";
};
D0F69E4F1D6B8BC40046BCD6 /* Gallery */ = {
isa = PBXGroup;
children = (
D0F69E501D6B8BDA0046BCD6 /* GalleryController.swift */,
D0F69E511D6B8BDA0046BCD6 /* GalleryControllerNode.swift */,
D0F69E521D6B8BDA0046BCD6 /* GalleryItem.swift */,
D0F69E531D6B8BDA0046BCD6 /* GalleryItemNode.swift */,
D0F69E541D6B8BDA0046BCD6 /* GalleryPagerNode.swift */,
D0F69E5A1D6B8BDD0046BCD6 /* Items */,
);
name = Gallery;
sourceTree = "<group>";
};
D0F69E5A1D6B8BDD0046BCD6 /* Items */ = {
isa = PBXGroup;
children = (
D0F69E5B1D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift */,
D0F69E5C1D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift */,
D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */,
D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */,
D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */,
D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */,
);
name = Items;
sourceTree = "<group>";
};
D0F69E671D6B8C030046BCD6 /* Map Input */ = {
isa = PBXGroup;
children = (
D0F69E681D6B8C160046BCD6 /* MapInputController.swift */,
D0F69E691D6B8C160046BCD6 /* MapInputControllerNode.swift */,
);
name = "Map Input";
sourceTree = "<group>";
};
D0F69E6C1D6B8C220046BCD6 /* Contacts */ = {
isa = PBXGroup;
children = (
D0F69E6D1D6B8C340046BCD6 /* ContactsController.swift */,
D0F69E6E1D6B8C340046BCD6 /* ContactsControllerNode.swift */,
D0F69E6F1D6B8C340046BCD6 /* ContactsPeerItem.swift */,
D0F69E701D6B8C340046BCD6 /* ContactsSearchContainerNode.swift */,
D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */,
D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */,
);
name = Contacts;
sourceTree = "<group>";
};
D0F69E791D6B8C3B0046BCD6 /* Settings */ = {
isa = PBXGroup;
children = (
D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */,
D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */,
);
name = Settings;
sourceTree = "<group>";
};
D0F69E7E1D6B8C500046BCD6 /* Supporting Files */ = {
isa = PBXGroup;
children = (
D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */,
D0F69E991D6B8D200046BCD6 /* UIImage+WebP.m */,
D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */,
D0F69E801D6B8C850046BCD6 /* FastBlur.m */,
D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */,
D0F69E821D6B8C850046BCD6 /* FFMpegSwResample.m */,
D0F69E831D6B8C850046BCD6 /* FrameworkBundle.swift */,
D0F69E841D6B8C850046BCD6 /* Localizable.swift */,
D0F69E851D6B8C850046BCD6 /* RingBuffer.h */,
D0F69E861D6B8C850046BCD6 /* RingBuffer.m */,
D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */,
D0F69EA51D6B8F3E0046BCD6 /* TelegramUIIncludes.h */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
D0F69E911D6B8C8E0046BCD6 /* Utils */ = {
isa = PBXGroup;
children = (
D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */,
D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */,
D0F69E941D6B8C9B0046BCD6 /* WebP.swift */,
);
name = Utils;
sourceTree = "<group>";
};
D0F69E9D1D6B8E240046BCD6 /* Resources */ = {
isa = PBXGroup;
children = (
D0F69E9E1D6B8E380046BCD6 /* FileResources.swift */,
D0F69E9F1D6B8E380046BCD6 /* PhotoResources.swift */,
D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */,
);
name = Resources;
sourceTree = "<group>";
};
D0FC40751D5B8E7400261D9D = {
isa = PBXGroup;
children = (
D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */,
D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */,
D0FC40811D5B8E7400261D9D /* TelegramUI */,
D0FC408C1D5B8E7500261D9D /* TelegramUITests */,
@@ -120,6 +735,12 @@
D0FC40811D5B8E7400261D9D /* TelegramUI */ = {
isa = PBXGroup;
children = (
D0F69E911D6B8C8E0046BCD6 /* Utils */,
D0F69DBB1D6B88330046BCD6 /* Media */,
D0F69DBD1D6B897A0046BCD6 /* Components */,
D0F69DE61D6B8A4E0046BCD6 /* Controllers */,
D0F69CCE1D6B87950046BCD6 /* Files */,
D0F69E7E1D6B8C500046BCD6 /* Supporting Files */,
D0FC40821D5B8E7400261D9D /* TelegramUI.h */,
D0FC40831D5B8E7400261D9D /* Info.plist */,
);
@@ -142,7 +763,11 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */,
D0FC40901D5B8E7500261D9D /* TelegramUI.h in Headers */,
D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */,
D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */,
D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -198,6 +823,7 @@
D0FC407E1D5B8E7400261D9D = {
CreatedOnToolsVersion = 8.0;
DevelopmentTeam = X834Q8SBVP;
LastSwiftMigration = 0800;
ProvisioningStyle = Manual;
};
D0FC40871D5B8E7500261D9D = {
@@ -231,6 +857,7 @@
buildActionMask = 2147483647;
files = (
D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */,
D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -248,6 +875,124 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */,
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */,
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */,
D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */,
D0F69E561D6B8BDA0046BCD6 /* GalleryControllerNode.swift in Sources */,
D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */,
D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */,
D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */,
D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */,
D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */,
D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */,
D0F69E031D6B8A880046BCD6 /* ChatListItem.swift in Sources */,
D0F69E081D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift in Sources */,
D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */,
D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */,
D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */,
D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */,
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */,
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */,
D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */,
D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */,
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */,
D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */,
D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */,
D0F69E651D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift in Sources */,
D0F69E421D6B8B7E0046BCD6 /* ChatInputView.swift in Sources */,
D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */,
D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */,
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */,
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */,
D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */,
D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */,
D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */,
D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */,
D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */,
D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */,
D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */,
D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */,
D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */,
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */,
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */,
D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */,
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */,
D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */,
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */,
D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */,
D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */,
D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */,
D0F69D671D6B87D30046BCD6 /* FFMpegPacket.swift in Sources */,
D0F69E321D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift in Sources */,
D0F69E041D6B8A880046BCD6 /* ChatListSearchItem.swift in Sources */,
D0F69E611D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift in Sources */,
D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */,
D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */,
D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */,
D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */,
D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */,
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */,
D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */,
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */,
D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */,
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */,
D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */,
D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */,
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */,
D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */,
D0F69E311D6B8B030046BCD6 /* ChatMessageBubbleItemNode.swift in Sources */,
D0F69E021D6B8A880046BCD6 /* ChatListHoleItem.swift in Sources */,
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */,
D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */,
D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */,
D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */,
D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */,
D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */,
D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */,
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */,
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */,
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */,
D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */,
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */,
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */,
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */,
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */,
D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */,
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */,
D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */,
D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */,
D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */,
D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */,
D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */,
D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */,
D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */,
D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */,
D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */,
D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */,
D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */,
D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */,
D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */,
D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */,
D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */,
D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */,
D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */,
D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */,
D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */,
D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */,
D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */,
D0F69E6B1D6B8C160046BCD6 /* MapInputControllerNode.swift in Sources */,
D0F69DCF1D6B8A0D0046BCD6 /* SearchBarNode.swift in Sources */,
D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */,
D0F69E2E1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift in Sources */,
D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */,
D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */,
D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -272,6 +1017,7 @@
/* Begin XCBuildConfiguration section */
D0400EDB1D5B900A007931CE /* Hockeyapp */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -318,8 +1064,10 @@
};
D0400EDC1D5B900A007931CE /* Hockeyapp */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
DEFINES_MODULE = YES;
@@ -331,6 +1079,11 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/libwebp/lib",
"$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib",
);
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -341,7 +1094,9 @@
};
D0400EDD1D5B900A007931CE /* Hockeyapp */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
DEVELOPMENT_TEAM = X834Q8SBVP;
INFOPLIST_FILE = TelegramUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -353,6 +1108,7 @@
};
D0FC40911D5B8E7500261D9D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -406,6 +1162,7 @@
};
D0FC40921D5B8E7500261D9D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -452,8 +1209,10 @@
};
D0FC40941D5B8E7500261D9D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEFINES_MODULE = YES;
@@ -465,18 +1224,26 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/libwebp/lib",
"$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib",
);
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
};
name = Debug;
};
D0FC40951D5B8E7500261D9D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
DEFINES_MODULE = YES;
@@ -488,6 +1255,11 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/libwebp/lib",
"$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib",
);
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -498,7 +1270,9 @@
};
D0FC40971D5B8E7500261D9D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
DEVELOPMENT_TEAM = X834Q8SBVP;
INFOPLIST_FILE = TelegramUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -510,7 +1284,9 @@
};
D0FC40981D5B8E7500261D9D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
DEVELOPMENT_TEAM = X834Q8SBVP;
INFOPLIST_FILE = TelegramUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";

View File

@@ -5,6 +5,22 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
@@ -26,6 +42,15 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
@@ -35,6 +60,15 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">

View File

@@ -7,7 +7,7 @@
<key>TelegramUI.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>19</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@@ -0,0 +1,69 @@
import Foundation
import Display
import AsyncDisplayKit
import Photos
private let testBackground = generateStretchableFilledCircleImage(radius: 8.0, color: UIColor.lightGray)
final class ActionSheetRollImageItem: ListViewItem {
let asset: PHAsset
init(asset: PHAsset) {
self.asset = asset
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ActionSheetRollImageItemNode()
node.contentSize = CGSize(width: 84.0, height: 84.0)
node.insets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)
node.updateAsset(asset: self.asset)
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), {
})
}
}
private final class ActionSheetRollImageItemNode: ListViewItemNode {
private let imageNode: ASImageNode
init() {
self.imageNode = ASImageNode()
self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 84.0, height: 84.0))
self.imageNode.displaysAsynchronously = true
self.imageNode.clipsToBounds = true
self.imageNode.cornerRadius = 8.0
//self.imageNode.contentMode = .scaleToFill
//self.imageNode.contentsScale = UIScreenScale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.imageNode)
}
func updateAsset(asset: PHAsset) {
let retinaSquare = CGSize(width: 84.0 * UIScreenScale, height: 84.0 * UIScreenScale)
let cropToSquare = PHImageRequestOptions()
cropToSquare.resizeMode = .exact;
let cropSideLength = min(asset.pixelWidth, asset.pixelHeight)
let square = CGRect(x: 0.0, y: 0.0, width: CGFloat(cropSideLength), height: CGFloat(cropSideLength))
let cropRect = square.applying(CGAffineTransform(scaleX: 1.0 / CGFloat(asset.pixelWidth), y: 1.0 / CGFloat(asset.pixelHeight)))
cropToSquare.normalizedCropRect = cropRect
PHImageManager.default().requestImage(for: asset, targetSize: retinaSquare, contentMode: .aspectFit, options: cropToSquare, resultHandler: { [weak self] image, result in
if let strongSelf = self, let image = image, let cgImage = image.cgImage {
let orientedImage = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
strongSelf.imageNode.image = orientedImage
}
})
}
}

View File

@@ -0,0 +1,7 @@
import Foundation
import Display
import AsyncDisplayKit
class AlertController {
}

View File

@@ -0,0 +1,84 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKit
import TelegramCore
enum AuthorizationCodeResult {
case Authorization(Api.auth.Authorization)
case Password
}
class AuthorizationCodeController: ViewController {
let account: UnauthorizedAccount
let phone: String
let sentCode: Api.auth.SentCode
var node: AuthorizationCodeControllerNode {
return self.displayNode as! AuthorizationCodeControllerNode
}
let signInDisposable = MetaDisposable()
let resultPipe = ValuePipe<AuthorizationCodeResult>()
var result: Signal<AuthorizationCodeResult, NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount, phone: String, sentCode: Api.auth.SentCode) {
self.account = account
self.phone = phone
self.sentCode = sentCode
super.init()
self.title = "Code"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationCodeController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.signInDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationCodeControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
}
@objc func nextPressed() {
var phoneCodeHash: String?
switch self.sentCode {
case let .sentCode(_, _, apiPhoneCodeHash, _, _):
phoneCodeHash = apiPhoneCodeHash
default:
break
}
if let phoneCodeHash = phoneCodeHash {
let signal = self.account.network.request(Api.functions.auth.signIn(phoneNumber: self.phone, phoneCodeHash: phoneCodeHash, phoneCode: node.codeNode.attributedText?.string ?? "")) |> map { authorization in
return AuthorizationCodeResult.Authorization(authorization)
} |> `catch` { error -> Signal<AuthorizationCodeResult, MTRpcError> in
switch (error.errorCode, error.errorDescription) {
case (401, "SESSION_PASSWORD_NEEDED"):
return .single(.Password)
case _:
return .fail(error)
}
}
self.signInDisposable.set(signal.start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.resultPipe.putNext(result)
}
}))
}
}
}

View File

@@ -0,0 +1,21 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationCodeControllerNode: ASDisplayNode {
let codeNode: ASEditableTextNode
override init() {
self.codeNode = ASEditableTextNode()
super.init()
self.codeNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.codeNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.codeNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.codeNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

View File

@@ -0,0 +1,84 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
public class AuthorizationController: NavigationController {
private var account: UnauthorizedAccount!
private let authorizedAccountValue = Promise<Account>()
public var authorizedAccount: Signal<Account, NoError> {
return authorizedAccountValue.get()
}
public init(account: UnauthorizedAccount) {
self.account = account
let phoneController = AuthorizationPhoneController(account: account)
super.init()
self.pushViewController(phoneController, animated: false)
let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal<Api.auth.Authorization, NoError> in
return deferred { [weak self] in
if let strongSelf = self {
strongSelf.account = account
let codeController = AuthorizationCodeController(account: account, phone: phone, sentCode: sentCode)
strongSelf.pushViewController(codeController, animated: true)
return codeController.result |> mapToSignal { result -> Signal<Api.auth.Authorization, NoError> in
switch result {
case let .Authorization(authorization):
return single(authorization, NoError.self)
case .Password:
return deferred { [weak self] () -> Signal<Api.auth.Authorization, NoError> in
if let strongSelf = self {
let passwordController = AuthorizationPasswordController(account: account)
strongSelf.pushViewController(passwordController, animated: true)
return passwordController.result
} else {
return complete(Api.auth.Authorization.self, NoError.self)
}
} |> runOn(Queue.mainQueue())
}
}
} else {
return complete(Api.auth.Authorization.self, NoError.self)
}
} |> runOn(Queue.mainQueue())
}
let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization -> Signal<Account, NoError> in
if let strongSelf = self {
switch authorization {
case let .authorization(user):
let user = TelegramUser(user: user)
return account.postbox.modify { modifier -> AccountState in
let state = AuthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, peerId: user.id, state: nil)
modifier.setState(state)
return state
} |> map { state -> Account in
return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id)
}
}
} else {
return .complete()
}
}
self.authorizedAccountValue.set(accountSignal)
}
override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKit
import TelegramCore
class AuthorizationPasswordController: ViewController {
private var account: UnauthorizedAccount
private var node: AuthorizationPasswordControllerNode {
return self.displayNode as! AuthorizationPasswordControllerNode
}
private let signInDisposable = MetaDisposable()
private let resultPipe = ValuePipe<Api.auth.Authorization>()
var result: Signal<Api.auth.Authorization, NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount) {
self.account = account
super.init()
self.title = "Password"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationPasswordController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
signInDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationPasswordControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
}
@objc func nextPressed() {
let password = self.node.passwordNode.attributedText?.string ?? ""
self.signInDisposable.set(verifyPassword(self.account, password: password).start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.resultPipe.putNext(result)
}
}))
}
}

View File

@@ -0,0 +1,21 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationPasswordControllerNode: ASDisplayNode {
let passwordNode: ASEditableTextNode
override init() {
self.passwordNode = ASEditableTextNode()
super.init()
self.passwordNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.passwordNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.passwordNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.passwordNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

View File

@@ -0,0 +1,75 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKit
import TelegramCore
class AuthorizationPhoneController: ViewController {
private var account: UnauthorizedAccount
private var node: AuthorizationPhoneControllerNode {
return self.displayNode as! AuthorizationPhoneControllerNode
}
private let codeDisposable = MetaDisposable()
private let resultPipe = ValuePipe<(UnauthorizedAccount, Api.auth.SentCode, String)>()
var result: Signal<(UnauthorizedAccount, Api.auth.SentCode, String), NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount) {
self.account = account
super.init()
self.title = "Telegram"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationPhoneController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
codeDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationPhoneControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
}
@objc func nextPressed() {
let phone = self.node.phoneNode.attributedText?.string ?? ""
let account = self.account
let sendCode = Api.functions.auth.sendCode(flags: 0, phoneNumber: phone, currentNumber: nil, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb", langCode: "en")
let signal = account.network.request(sendCode)
|> map { result in
return (result, account)
} |> `catch` { error -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in
switch error.errorDescription {
case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"):
let range = error.errorDescription.range(of: "MIGRATE_")!
let updatedMasterDatacenterId = Int32(error.errorDescription.substring(from: range.upperBound))!
let updatedAccount = account.changedMasterDatacenterId(updatedMasterDatacenterId)
return updatedAccount.network.request(sendCode) |> map { sentCode in return (sentCode, updatedAccount) }
case _:
return .fail(error)
}
}
codeDisposable.set(signal.start(next: { [weak self] (result, account) in
if let strongSelf = self {
strongSelf.account = account
strongSelf.resultPipe.putNext((account, result, phone))
}
}))
}
}

View File

@@ -0,0 +1,21 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationPhoneControllerNode: ASDisplayNode {
let phoneNode: ASEditableTextNode
override init() {
self.phoneNode = ASEditableTextNode()
super.init()
self.phoneNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.phoneNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.phoneNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.phoneNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

40
TelegramUI/Cache.swift Normal file
View File

@@ -0,0 +1,40 @@
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,846 @@
import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
private enum ChatControllerScrollPosition {
case Unread(index: MessageIndex)
case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
}
private enum ChatHistoryViewUpdateType {
case Initial(fadeIn: Bool)
case Generic(type: ViewUpdateType)
}
private enum ChatHistoryViewUpdate {
case Loading
case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatControllerScrollPosition?)
}
private struct ChatHistoryView {
let originalView: MessageHistoryView
let filteredEntries: [ChatHistoryEntry]
}
private enum ChatHistoryViewTransitionReason {
case Initial(fadeIn: Bool)
case InteractiveChanges
case HoleChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection])
case Reload
}
private struct ChatHistoryViewTransition {
let historyView: ChatHistoryView
let deleteItems: [ListViewDeleteItem]
let insertItems: [ListViewInsertItem]
let updateItems: [ListViewUpdateItem]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
private func messageHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?) -> Signal<ChatHistoryViewUpdate, NoError> {
switch location {
case let .Initial(count):
var preloaded = false
var fadeIn = false
return account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
} else {
if let maxReadIndex = view.maxReadIndex {
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= maxReadIndex {
targetIndex = i
break
}
}
let maxIndex = min(view.entries.count, targetIndex + count / 2)
if maxIndex >= targetIndex {
for i in targetIndex ..< maxIndex {
if case .HoleEntry = view.entries[i] {
fadeIn = true
return .Loading
}
}
}
preloaded = true
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex))
} else {
preloaded = true
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil)
}
}
}
case let .InitialSearch(messageId, count):
var preloaded = false
var fadeIn = false
return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
} else {
let anchorIndex = view.anchorIndex
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= anchorIndex {
targetIndex = i
break
}
}
let maxIndex = min(view.entries.count, targetIndex + count / 2)
if maxIndex >= targetIndex {
for i in targetIndex ..< maxIndex {
if case .HoleEntry = view.entries[i] {
fadeIn = true
return .Loading
}
}
}
preloaded = true
//case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false))
}
}
case let .Navigation(index, anchorIndex):
trace("messageHistoryViewForLocation navigation \(index.id.id)")
var first = true
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
let genericType: ViewUpdateType
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil)
}
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let chatScrollPosition = ChatControllerScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
let genericType: ViewUpdateType
let scrollPosition: ChatControllerScrollPosition? = first ? chatScrollPosition : nil
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition)
}
}
}
private func historyEntriesForView(_ view: MessageHistoryView) -> [ChatHistoryEntry] {
var entries: [ChatHistoryEntry] = []
for entry in view.entries {
switch entry {
case let .HoleEntry(hole, _):
entries.append(.HoleEntry(hole))
case let .MessageEntry(message, _):
entries.append(.MessageEntry(message))
}
}
if let maxReadIndex = view.maxReadIndex {
var inserted = false
var i = 0
let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex)
for entry in entries {
if entry > unreadEntry {
entries.insert(unreadEntry, at: i)
inserted = true
break
}
i += 1
}
if !inserted {
//entries.append(.UnreadEntry(maxReadIndex))
}
}
return entries
}
private func preparedHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatControllerScrollPosition?) -> Signal<ChatHistoryViewTransition, NoError> {
return Signal { subscriber in
let updateIndices: [(Int, ChatHistoryEntry)] = []
//let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount: Int
if let fromView = fromView {
previousCount = fromView.filteredEntries.count
} else {
previousCount = 0;
}
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
var adjustedIndicesAndItems: [ListViewInsertItem] = []
var adjustedUpdateItems: [ListViewUpdateItem] = []
let updatedCount = toView.filteredEntries.count
var options: ListViewDeleteAndInsertOptions = []
var maxAnimatedInsertionIndex = -1
var stationaryItemRange: (Int, Int)?
var scrollToItem: ListViewScrollToItem?
switch reason {
case let .Initial(fadeIn):
if fadeIn {
let _ = options.insert(.AnimateAlpha)
} else {
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
}
case .InteractiveChanges:
let _ = options.insert(.AnimateAlpha)
let _ = options.insert(.AnimateInsertion)
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
case .Reload:
break
case let .HoleChanges(filledHoleDirections, removeHoleDirections):
if let (_, removeDirection) = removeHoleDirections.first {
switch removeDirection {
case .LowerToUpper:
var holeIndex: MessageIndex?
for (index, _) in filledHoleDirections {
if holeIndex == nil || index < holeIndex! {
holeIndex = index
}
}
if let holeIndex = holeIndex {
for i in 0 ..< toView.filteredEntries.count {
if toView.filteredEntries[i].index >= holeIndex {
let index = toView.filteredEntries.count - 1 - (i - 1)
stationaryItemRange = (index, Int.max)
break
}
}
}
case .UpperToLower:
break
case .AroundIndex:
break
}
}
}
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
let adjustedPrevousIndex: Int?
if let previousIndex = previousIndex {
adjustedPrevousIndex = previousCount - 1 - previousIndex
} else {
adjustedPrevousIndex = nil
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
switch entry {
case let .MessageEntry(message):
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message), directionHint: directionHint))
case .HoleEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatHoleItem(), directionHint: directionHint))
case .UnreadEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatUnreadItem(), directionHint: directionHint))
}
}
for (index, entry) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let directionHint: ListViewItemOperationDirectionHint? = nil
switch entry {
case let .MessageEntry(message):
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message), directionHint: directionHint))
case .HoleEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatHoleItem(), directionHint: directionHint))
case .UnreadEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatUnreadItem(), directionHint: directionHint))
}
}
if let scrollPosition = scrollPosition {
switch scrollPosition {
case let .Unread(unreadIndex):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if case .UnreadEntry = entry {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index -= 1
}
if scrollToItem == nil {
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.index >= unreadIndex {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index -= 1
}
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.index < unreadIndex {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index += 1
}
}
case let .Index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.index >= scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.index < scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index += 1
}
}
}
}
subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertItems: adjustedIndicesAndItems, updateItems: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
subscriber.putCompletion()
return EmptyDisposable
}
}
private func maxIncomingMessageIdForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageId? {
for i in (indexRange.0 ... indexRange.1).reversed() {
if case let .MessageEntry(message) = entries[i], message.flags.contains(.Incoming) {
return message.id
}
}
return nil
}
private var useDarkMode = false
public class ChatController: ViewController {
private var containerLayout = ContainerViewLayout()
private let account: Account
private let peerId: PeerId
private let messageId: MessageId?
private var historyView: ChatHistoryView?
private let peerDisposable = MetaDisposable()
private let historyDisposable = MetaDisposable()
private let readHistoryDisposable = MetaDisposable()
private let messageViewQueue = Queue()
private let messageIndexDisposable = MetaDisposable()
private var enqueuedHistoryViewTransition: (ChatHistoryViewTransition, () -> Void)?
private var layoutActionOnViewTransition: (@escaping () -> Void)?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let maxVisibleIncomingMessageId = Promise<MessageId>()
private let canReadHistory = Promise<Bool>()
private let _chatHistoryLocation = Promise<ChatHistoryLocation>()
private var chatHistoryLocation: Signal<ChatHistoryLocation, NoError> {
return self._chatHistoryLocation.get()
}
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
private var controllerInteraction: ChatControllerInteraction?
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) {
self.account = account
self.peerId = peerId
self.messageId = messageId
super.init()
self.setupThemeWithDarkMode(useDarkMode)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: strongSelf.peerId), anchorIndex: MessageIndex.lowerBound(peerId: strongSelf.peerId), sourceIndex: MessageIndex.upperBound(peerId: strongSelf.peerId), scrollPosition: .Bottom, animated: true)))
}
}
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in
if let strongSelf = self, let historyView = strongSelf.historyView {
var galleryMedia: Media?
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
for media in message.media {
if let file = media as? TelegramMediaFile {
galleryMedia = file
} else if let image = media as? TelegramMediaImage {
galleryMedia = image
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file {
galleryMedia = file
} else if let image = content.image {
galleryMedia = image
}
}
}
break
}
if let galleryMedia = galleryMedia {
if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" {
debugPlayMedia(account: strongSelf.account, file: file)
} else {
let gallery = GalleryController(account: strongSelf.account, messageId: id)
strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in
if let strongSelf = strongSelf {
if let messageIdAndMedia = messageIdAndMedia {
strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]]
} else {
strongSelf.controllerInteraction?.hiddenMedia = [:]
}
strongSelf.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHiddenMedia()
}
}
}
}))
strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionNode: { [weak self] messageId, media in
if let strongSelf = self {
var transitionNode: ASDisplayNode?
strongSelf.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media) {
transitionNode = result
}
}
}
return transitionNode
}
return nil
}))
}
}
}
}, testNavigateToMessage: { [weak self] fromId, id in
if let strongSelf = self, let historyView = strongSelf.historyView {
var fromIndex: MessageIndex?
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == fromId {
fromIndex = MessageIndex(message)
break
}
if let fromIndex = fromIndex {
var found = false
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
found = true
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex(message), anchorIndex: MessageIndex(message), sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)))
}
if !found {
strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in
if let strongSelf = strongSelf, let index = index {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index:index, anchorIndex: index, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)))
}
}))
}
}
}
})
self.controllerInteraction = controllerInteraction
let messageViewQueue = self.messageViewQueue
peerDisposable.set((account.postbox.peerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.title = peer.displayTitle
}
}))
let fixedCombinedReadState = Atomic<CombinedPeerReadState?>(value: nil)
let historyViewUpdate = self.chatHistoryLocation
|> distinctUntilChanged
|> mapToSignal { location in
return messageHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: nil) |> beforeNext { viewUpdate in
switch viewUpdate {
case let .HistoryView(view, _, _):
let _ = fixedCombinedReadState.swap(view.combinedReadState)
default:
break
}
}
}
let previousView = Atomic<ChatHistoryView?>(value: nil)
let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal<ChatHistoryViewTransition, NoError> in
switch update {
case .Loading:
Queue.mainQueue().async { [weak self] in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
}
return .complete()
case let .HistoryView(view, type, scrollPosition):
let reason: ChatHistoryViewTransitionReason
var prepareOnMainQueue = false
switch type {
case let .Initial(fadeIn):
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn)
prepareOnMainQueue = !fadeIn
case let .Generic(genericType):
switch genericType {
case .InitialUnread:
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false)
case .Generic:
reason = ChatHistoryViewTransitionReason.InteractiveChanges
case .UpdateVisible:
reason = ChatHistoryViewTransitionReason.Reload
case let .FillHole(insertions, deletions):
reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions)
}
}
let processedView = ChatHistoryView(originalView: view, filteredEntries: historyEntriesForView(view))
let previous = previousView.swap(processedView)
return preparedHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition) |> runOn( prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
}
}
let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
if let strongSelf = self {
return strongSelf.enqueueHistoryViewTransition(transition)
}
return .complete()
}
self.historyDisposable.set(appliedTransition.start())
let previousMaxIncomingMessageId = Atomic<MessageId?>(value: nil)
let readHistory = combineLatest(self.maxVisibleIncomingMessageId.get(), self.canReadHistory.get())
|> map { messageId, canRead in
if canRead {
var apply = false
let _ = previousMaxIncomingMessageId.modify { previousId in
if previousId == nil || previousId! < messageId {
apply = true
return messageId
} else {
return previousId
}
}
if apply {
let _ = account.postbox.modify({ modifier in
modifier.applyInteractiveReadMaxId(messageId)
}).start()
}
}
}
self.readHistoryDisposable.set(readHistory.start())
if let messageId = messageId {
self._chatHistoryLocation.set(.single(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 60)))
} else {
self._chatHistoryLocation.set(.single(ChatHistoryLocation.Initial(count: 60)))
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.historyDisposable.dispose()
self.readHistoryDisposable.dispose()
self.messageIndexDisposable.dispose()
self.galleryHiddenMesageAndMediaDisposable.dispose()
}
private func setupThemeWithDarkMode(_ darkMode: Bool) {
if darkMode {
self.statusBar.style = .White
self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.9)
self.navigationBar.foregroundColor = UIColor.white
self.navigationBar.accentColor = UIColor.white
self.navigationBar.stripeColor = UIColor.black
} else {
self.statusBar.style = .Black
self.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
self.navigationBar.foregroundColor = UIColor.black
self.navigationBar.accentColor = UIColor(0x1195f2)
self.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)
}
}
var chatDisplayNode: ChatControllerNode {
get {
return super.displayNode as! ChatControllerNode
}
}
override public func loadDisplayNode() {
self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId)
self.chatDisplayNode.listView.displayedItemRangeChanged = { [weak self] displayedRange in
if let strongSelf = self {
/*if let transactionTag = strongSelf.listViewTransactionTag {
strongSelf.messageViewQueue.dispatch {
if transactionTag == strongSelf.historyViewTransactionTag {
if let range = range, historyView = strongSelf.historyView, firstEntry = historyView.filteredEntries.first, lastEntry = historyView.filteredEntries.last {
if range.firstIndex < 5 && historyView.originalView.laterId != nil {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex)))
} else if range.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex)))
} else {
//strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(messageView.id, earliestVisibleIndex: viewEntries[viewEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: viewEntries[viewEntries.count - 1 - range.firstIndex].index)
}
}
}
}
}*/
if let visible = displayedRange.visibleRange, let historyView = strongSelf.historyView {
if let messageId = maxIncomingMessageIdForEntries(historyView.filteredEntries, indexRange: (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)) {
strongSelf.updateMaxVisibleReadIncomingMessageId(messageId)
}
}
}
}
self.chatDisplayNode.listView.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
if let offset = offset, offset < 40.0 {
if strongSelf.chatDisplayNode.navigateToLatestButton.alpha == 1.0 {
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: {
strongSelf.chatDisplayNode.navigateToLatestButton.alpha = 0.0
}, completion: nil)
}
} else {
if strongSelf.chatDisplayNode.navigateToLatestButton.alpha == 0.0 {
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: {
strongSelf.chatDisplayNode.navigateToLatestButton.alpha = 1.0
}, completion: nil)
}
}
}
}
self.chatDisplayNode.requestLayout = { [weak self] animated in
self?.requestLayout(transition: animated ? .animated(duration: 0.1, curve: .easeInOut) : .immediate)
}
self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in
self?.layoutActionOnViewTransition = f
}
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
if let strongSelf = self {
let controller = ChatMediaActionSheetController()
controller.location = { [weak strongSelf] in
if let strongSelf = strongSelf {
let mapInputController = MapInputController()
strongSelf.present(mapInputController, in: .window)
}
}
controller.contacts = { [weak strongSelf] in
if let strongSelf = strongSelf {
useDarkMode = !useDarkMode
strongSelf.setupThemeWithDarkMode(useDarkMode)
}
}
strongSelf.present(controller, in: .window)
}
}
self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in
if let strongSelf = self {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: strongSelf.peerId), anchorIndex: MessageIndex.upperBound(peerId: strongSelf.peerId), sourceIndex: MessageIndex.lowerBound(peerId: strongSelf.peerId), scrollPosition: .Top, animated: true)))
}
}
self.displayNodeDidLoad()
self.dequeueHistoryViewTransition()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.chatDisplayNode.listView.preloadPages = true
self.canReadHistory.set(.single(true))
}
private func enqueueHistoryViewTransition(_ transition: ChatHistoryViewTransition) -> Signal<Void, NoError> {
return Signal { [weak self] subscriber in
if let strongSelf = self {
if let _ = strongSelf.enqueuedHistoryViewTransition {
preconditionFailure()
}
strongSelf.enqueuedHistoryViewTransition = (transition, {
subscriber.putCompletion()
})
if strongSelf.isNodeLoaded {
strongSelf.dequeueHistoryViewTransition()
} else {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
} else {
subscriber.putCompletion()
}
return EmptyDisposable
} |> runOn(Queue.mainQueue())
}
private func updateMaxVisibleReadIncomingMessageId(_ id: MessageId) {
self.maxVisibleIncomingMessageId.set(.single(id))
}
private func dequeueHistoryViewTransition() {
if let (transition, completion) = self.enqueuedHistoryViewTransition {
self.enqueuedHistoryViewTransition = nil
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.historyView = transition.historyView
if let range = visibleRange.loadedRange {
strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index)
if let visible = visibleRange.visibleRange {
if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) {
strongSelf.updateMaxVisibleReadIncomingMessageId(messageId)
}
}
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
completion()
}
}
if let layoutActionOnViewTransition = self.layoutActionOnViewTransition {
self.layoutActionOnViewTransition = nil
layoutActionOnViewTransition()
self.chatDisplayNode.containerLayoutUpdated(self.containerLayout, navigationBarHeight: self.navigationBar.frame.maxY, transition: .animated(duration: 0.5 * 1.3, curve: .spring), listViewTransaction: { updateSizeAndInsets in
var options = transition.options
let _ = options.insert(.Synchronous)
let _ = options.insert(.LowLatency)
options.remove(.AnimateInsertion)
let deleteItems = transition.deleteItems.map({ item in
return ListViewDeleteItem(index: item.index, directionHint: nil)
})
var maxInsertedItem: Int?
var insertItems: [ListViewInsertItem] = []
for i in 0 ..< transition.insertItems.count {
let item = transition.insertItems[i]
if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) {
maxInsertedItem = item.index
}
insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil))
}
let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(speed: 1.3), directionHint: .Up)
var stationaryItemRange: (Int, Int)?
if let maxInsertedItem = maxInsertedItem {
stationaryItemRange = (maxInsertedItem + 1, Int.max)
}
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: deleteItems, insertIndicesAndItems: insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, completion: completion)
})
} else {
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, completion: completion)
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.containerLayout = layout
self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets in
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in })
})
}
}

View File

@@ -0,0 +1,14 @@
import Foundation
import Postbox
import AsyncDisplayKit
public final class ChatControllerInteraction {
let openMessage: (MessageId) -> Void
let testNavigateToMessage: (MessageId, MessageId) -> Void
var hiddenMedia: [MessageId: [Media]] = [:]
public init(openMessage: @escaping (MessageId) -> Void, testNavigateToMessage: @escaping (MessageId, MessageId) -> Void) {
self.openMessage = openMessage
self.testNavigateToMessage = testNavigateToMessage
}
}

View File

@@ -0,0 +1,183 @@
import Foundation
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import Display
import TelegramCore
private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0")
enum ChatMessageViewPosition: Equatable {
case AroundUnread(count: Int)
case Around(index: MessageIndex, anchorIndex: MessageIndex)
case Scroll(index: MessageIndex, anchorIndex: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition)
}
func ==(lhs: ChatMessageViewPosition, rhs: ChatMessageViewPosition) -> Bool {
switch lhs {
case let .Around(lhsId, lhsAnchorIndex):
switch rhs {
case let .Around(rhsId, rhsAnchorIndex) where lhsId == rhsId && lhsAnchorIndex == rhsAnchorIndex:
return true
default:
return false
}
case let .Scroll(lhsIndex, lhsAnchorIndex, lhsSourceIndex, lhsScrollPosition):
switch rhs {
case let .Scroll(rhsIndex, rhsAnchorIndex, rhsSourceIndex, rhsScrollPosition) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex && lhsSourceIndex == rhsSourceIndex && lhsScrollPosition == rhsScrollPosition:
return true
default:
return false
}
case let .AroundUnread(lhsCount):
switch rhs {
case let .AroundUnread(rhsCount) where lhsCount == rhsCount:
return true
default:
return false
}
}
}
class ChatControllerNode: ASDisplayNode {
let account: Account
let peerId: PeerId
let backgroundNode: ASDisplayNode
let listView: ListView
let inputNode: ChatInputNode
let navigateToLatestButton: ChatHistoryNavigationButtonNode
private var ignoreUpdateHeight = false
var displayAttachmentMenu: () -> Void = { }
var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in }
var requestLayout: (Bool) -> Void = { _ in }
init(account: Account, peerId: PeerId) {
self.account = account
self.peerId = peerId
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.contentMode = .scaleAspectFill
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.clipsToBounds = true
self.listView = ListView()
self.listView.preloadPages = false
//self.listView.debugInfo = true
self.inputNode = ChatInputNode()
self.navigateToLatestButton = ChatHistoryNavigationButtonNode()
self.navigateToLatestButton.alpha = 0.0
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor(0xdee3e9)
self.backgroundNode.contents = backgroundImage?.cgImage
self.addSubnode(self.backgroundNode)
self.listView.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0)
self.addSubnode(self.listView)
self.addSubnode(self.inputNode)
self.addSubnode(self.navigateToLatestButton)
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.inputNode.updateHeight = { [weak self] in
if let strongSelf = self, !strongSelf.ignoreUpdateHeight {
strongSelf.requestLayout(true)
}
}
self.inputNode.sendMessage = { [weak self] in
if let strongSelf = self {
if strongSelf.inputNode.textInputNode?.isFirstResponder() ?? false {
applyKeyboardAutocorrection()
}
let text = strongSelf.inputNode.text
strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.ignoreUpdateHeight = true
strongSelf.inputNode.text = ""
strongSelf.ignoreUpdateHeight = false
}
})
let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text).start()
}
}
self.inputNode.displayAttachmentMenu = { [weak self] in
self?.displayAttachmentMenu()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) {
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 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 messageTextInputSize = self.inputNode.calculateSizeThatFits(CGSize(width: layout.size.width, height: min(layout.size.height / 2.0, 240.0)))
self.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
let listViewCurve: ListViewAnimationCurve
var speedFactor: CGFloat = 1.0
if curve == 7 {
speedFactor = CGFloat(duration) / 0.5
listViewCurve = .Spring(speed: CGFloat(speedFactor))
} else {
listViewCurve = .Default
}
let inputViewFrame = CGRect(x: 0.0, y: layout.size.height - messageTextInputSize.height - insets.bottom, width: layout.size.width, height: messageTextInputSize.height)
listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.bottom + inputViewFrame.size.height + 4.0, left: insets.right, bottom: insets.top, right: insets.left), duration: duration, curve: listViewCurve))
let navigateToLatestButtonSize = self.navigateToLatestButton.bounds.size
let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: inputViewFrame.minY - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize)
if duration > DBL_EPSILON {
UIView.animate(withDuration: duration / Double(speedFactor), delay: 0.0, options: UIViewAnimationOptions(rawValue: curve << 16), animations: {
self.inputNode.frame = inputViewFrame
self.navigateToLatestButton.frame = navigateToLatestButtonFrame
}, completion: nil)
} else {
self.inputNode.frame = inputViewFrame
self.navigateToLatestButton.frame = navigateToLatestButtonFrame
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended {
self.view.endEditing(true)
}
}
}

View File

@@ -0,0 +1,138 @@
import Foundation
import Postbox
import Display
import SwiftSignalKit
import WebKit
import TelegramCore
class ChatDocumentGalleryItem: GalleryItem {
let account: Account
let message: Message
let location: MessageHistoryEntryLocation?
init(account: Account, message: Message, location: MessageHistoryEntryLocation?) {
self.account = account
self.message = message
self.location = location
}
func node() -> GalleryItemNode {
let node = ChatDocumentGalleryItemNode()
for media in self.message.media {
if let file = media as? TelegramMediaFile {
node.setFile(account: account, file: file)
break
}
}
if let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
return node
}
func updateNode(node: GalleryItemNode) {
if let node = node as? ChatDocumentGalleryItemNode, let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
}
}
class ChatDocumentGalleryItemNode: GalleryItemNode {
fileprivate let _title = Promise<String>()
private let webView: UIView
private var accountAndFile: (Account, TelegramMediaFile)?
private let dataDisposable = MetaDisposable()
private var isVisible = false
override init() {
if #available(iOS 9.0, *) {
let webView = WKWebView()
self.webView = webView
} else {
let webView = UIWebView()
webView.scalesPageToFit = true
self.webView = webView
}
super.init()
self.view.addSubview(self.webView)
}
deinit {
self.dataDisposable.dispose()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))
}
override func navigationStyle() -> Signal<GalleryItemNodeNavigationStyle, NoError> {
return .single(.light)
}
func setFile(account: Account, file: TelegramMediaFile) {
let updateFile = self.accountAndFile?.1 != file
self.accountAndFile = (account, file)
if updateFile {
self.maybeLoadContent()
}
}
private func maybeLoadContent() {
if let (account, file) = self.accountAndFile {
var pathExtension: String?
if let fileName = file.fileName {
pathExtension = (fileName as NSString).pathExtension
}
let data = account.postbox.mediaBox.resourceData(CloudFileMediaResource(location: file.location, size: file.size), pathExtension: pathExtension, complete: true)
|> deliverOnMainQueue
self.dataDisposable.set(data.start(next: { [weak self] data in
if let strongSelf = self {
if data.size == file.size {
if let webView = strongSelf.webView as? WKWebView {
if #available(iOS 9.0, *) {
webView.loadFileURL(URL(fileURLWithPath: data.path), allowingReadAccessTo: URL(fileURLWithPath: data.path))
}
} else if let webView = strongSelf.webView as? UIWebView {
webView.loadRequest(URLRequest(url: URL(fileURLWithPath: data.path)))
}
}
}
}))
}
}
/*private func unloadContent() {
self.dataDisposable.set(nil)
self.webView.stopLoading()
self.webView.loadHTMLString("<html></html>", baseURL: nil)
}*/
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
/*if self.isVisible != isVisible {
self.isVisible = isVisible
if isVisible {
self.maybeLoadContent()
} else {
self.unloadContent()
}
}*/
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
}

View File

@@ -0,0 +1,74 @@
import Postbox
import TelegramCore
enum ChatHistoryEntry: Identifiable, Comparable {
case HoleEntry(MessageHistoryHole)
case MessageEntry(Message)
case UnreadEntry(MessageIndex)
var stableId: UInt64 {
switch self {
case let .HoleEntry(hole):
return UInt64(hole.stableId) | ((UInt64(1) << 40))
case let .MessageEntry(message):
return UInt64(message.stableId) | ((UInt64(2) << 40))
case .UnreadEntry:
return UInt64(3) << 40
}
}
var index: MessageIndex {
switch self {
case let .HoleEntry(hole):
return hole.maxIndex
case let .MessageEntry(message):
return MessageIndex(message)
case let .UnreadEntry(index):
return index
}
}
}
func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
switch lhs {
case let .HoleEntry(lhsHole):
switch rhs {
case let .HoleEntry(rhsHole) where lhsHole == rhsHole:
return true
default:
return false
}
case let .MessageEntry(lhsMessage):
switch rhs {
case let .MessageEntry(rhsMessage) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags:
if lhsMessage.media.count != rhsMessage.media.count {
return false
}
for i in 0 ..< lhsMessage.media.count {
if !lhsMessage.media[i].isEqual(rhsMessage.media[i]) {
return false
}
}
return true
default:
return false
}
case let .UnreadEntry(lhsIndex):
switch rhs {
case let .UnreadEntry(rhsIndex) where lhsIndex == rhsIndex:
return true
default:
return false
}
}
}
func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
let lhsIndex = lhs.index
let rhsIndex = rhs.index
if lhsIndex == rhsIndex {
return lhs.stableId < rhs.stableId
} else {
return lhsIndex < rhsIndex
}
}

View File

@@ -0,0 +1,23 @@
import Postbox
import Display
enum ChatHistoryLocation: Equatable {
case Initial(count: Int)
case InitialSearch(messageId: MessageId, count: Int)
case Navigation(index: MessageIndex, anchorIndex: MessageIndex)
case Scroll(index: MessageIndex, anchorIndex: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool)
}
func ==(lhs: ChatHistoryLocation, rhs: ChatHistoryLocation) -> Bool {
switch lhs {
case let .Navigation(lhsIndex, lhsAnchorIndex):
switch rhs {
case let .Navigation(rhsIndex, rhsAnchorIndex) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex:
return true
default:
return false
}
default:
return false
}
}

View File

@@ -0,0 +1,52 @@
import Foundation
import AsyncDisplayKit
import Display
private func generateBackgroundImage() -> UIImage? {
return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0)))
context.setLineWidth(0.5)
context.setStrokeColor(UIColor(0x000000, 0.15).cgColor)
context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5)))
context.setStrokeColor(UIColor(0x88888D).cgColor)
context.setLineWidth(1.5)
let position = CGPoint(x: 9.0 - 0.5, y: 23.0)
context.move(to: CGPoint(x: position.x + 1.0, y: position.y - 1.0))
context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0))
context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0))
context.strokePath()
})
}
private let backgroundImage = generateBackgroundImage()
class ChatHistoryNavigationButtonNode: ASControlNode {
private let imageNode: ASImageNode
var tapped: (() -> Void)?
override init() {
self.imageNode = ASImageNode()
self.imageNode.displayWithoutProcessing = true
self.imageNode.image = backgroundImage
self.imageNode.isLayerBacked = true
super.init()
self.addSubnode(self.imageNode)
self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
self.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
self.addTarget(self, action: #selector(onTap), forControlEvents: .touchUpInside)
}
@objc func onTap() {
if let tapped = self.tapped {
tapped()
}
}
}

View File

@@ -0,0 +1,21 @@
import Foundation
import Display
import AsyncDisplayKit
final class ChatHoleGalleryItem: GalleryItem {
func node() -> GalleryItemNode {
return ChatHoleGalleryItemNode()
}
func updateNode(node: GalleryItemNode) {
}
}
final class ChatHoleGalleryItemNode: GalleryItemNode {
override init() {
super.init()
self.backgroundColor = UIColor.blue
}
}

View File

@@ -0,0 +1,75 @@
import Foundation
import UIKit
import Postbox
import AsyncDisplayKit
import Display
private func backgroundImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x748391, 0.45).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
}
private let titleFont = UIFont.systemFont(ofSize: 13.0)
class ChatHoleItem: ListViewItem {
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatHoleItemNode()
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
completion(node, {})
}
}
}
class ChatHoleItemNode: ListViewItemNode {
let backgroundNode: ASImageNode
let labelNode: TextNode
init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.labelNode = TextNode()
self.labelNode.isLayerBacked = true
super.init(layerBacked: true)
self.backgroundNode.image = backgroundImage(color: UIColor.blue)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.labelNode)
self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let (layout, apply) = self.asyncLayout()(width)
apply()
self.contentSize = layout.contentSize
self.insets = layout.insets
}
func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
return { width in
let (size, apply) = labelLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor.white), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil)
let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0)
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in
if let strongSelf = self {
let _ = apply()
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size)
}
})
}
}
}

View File

@@ -0,0 +1,214 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
class ChatImageGalleryItem: GalleryItem {
let account: Account
let message: Message
let location: MessageHistoryEntryLocation?
init(account: Account, message: Message, location: MessageHistoryEntryLocation?) {
self.account = account
self.message = message
self.location = location
}
func node() -> GalleryItemNode {
let node = ChatImageGalleryItemNode()
for media in self.message.media {
if let image = media as? TelegramMediaImage {
node.setImage(account: account, image: image)
break
} else if let file = media as? TelegramMediaFile, file.mimeType.hasPrefix("image/") {
node.setFile(account: account, file: file)
break
}
}
if let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
return node
}
func updateNode(node: GalleryItemNode) {
if let node = node as? ChatImageGalleryItemNode, let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
}
}
final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
private let imageNode: TransformImageNode
fileprivate let _ready = Promise<Void>()
fileprivate let _title = Promise<String>()
private var accountAndMedia: (Account, Media)?
private var fetchDisposable = MetaDisposable()
override init() {
self.imageNode = TransformImageNode()
super.init()
self.imageNode.imageUpdated = { [weak self] in
self?._ready.set(.single(Void()))
}
self.imageNode.view.contentMode = .scaleAspectFill
self.imageNode.clipsToBounds = true
/*self.imageNode.layer.shadowRadius = 80.0
self.imageNode.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
self.imageNode.layer.shadowOffset = CGSize(width: 0.0, height: 40.0)
self.imageNode.layer.shadowOpacity = 0.5*/
}
deinit {
self.fetchDisposable.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
fileprivate func setImage(account: Account, image: TelegramMediaImage) {
if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) {
if let largestSize = largestRepresentationForPhoto(image) {
let displaySize = largestSize.dimensions.dividedByScreenScale()
self.imageNode.alphaTransitionOnFirstUpdate = false
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false)
self.zoomableContent = (largestSize.dimensions, self.imageNode)
self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(CloudFileMediaResource(location: largestSize.location, size: largestSize.size ?? 0)).start())
} else {
self._ready.set(.single(Void()))
}
}
self.accountAndMedia = (account, image)
}
func setFile(account: Account, file: TelegramMediaFile) {
if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) {
if let largestSize = file.dimensions {
self.imageNode.alphaTransitionOnFirstUpdate = false
let displaySize = largestSize.dividedByScreenScale()
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: true), dispatchOnDisplayLink: false)
self.zoomableContent = (largestSize, self.imageNode)
} else {
self._ready.set(.single(Void()))
}
}
self.accountAndMedia = (account, file)
}
override func animateIn(from node: ASDisplayNode) {
var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview)
let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view)
let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view)
/*let image = generateImage(node.view.bounds.size, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translate(x: size.width / 2.0, y: size.height / 2.0)
context.scale(x: 1.0, y: -1.0)
context.translate(x: -size.width / 2.0, y: -size.height / 2.0)
//node.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false)
node.layer.render(in: context)
})*/
//let copyView = UIImageView(image: image)
let copyView = node.view.snapshotContentTree()!
self.view.insertSubview(copyView, belowSubview: self.scrollView)
copyView.frame = transformedSelfFrame
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
//copyView.layer.animateFrame(from: transformedSelfFrame, to: transformedCopyViewFinalFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
self.imageNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.imageNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
transformedFrame.origin = CGPoint()
self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
override func animateOut(to node: ASDisplayNode, completion: @escaping () -> Void) {
var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview)
let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let copyView = node.view.snapshotContentTree()!
self.view.insertSubview(copyView, belowSubview: self.scrollView)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
self.imageNode.layer.animatePosition(from: self.imageNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile {
if isVisible {
self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(CloudFileMediaResource(location: file.location, size: file.size)).start())
} else {
self.fetchDisposable.set(nil)
}
}
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
}

View File

@@ -0,0 +1,199 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import WebKit
private let textInputViewBackground: UIImage = {
let diameter: CGFloat = 10.0
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(UIColor(0xfafafa).cgColor)
context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
context.setStrokeColor(UIColor(0xc7c7cc).cgColor)
let strokeWidth: CGFloat = 0.5
context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth))
let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
UIGraphicsEndImageContext()
return image
}()
private let attachmentIcon = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.precomposed()
class ChatInputNode: ASDisplayNode, ASEditableTextNodeDelegate {
var textPlaceholderNode: TextNode
var textInputNode: ASEditableTextNode?
let textInputBackgroundView: UIImageView
let sendButton: UIButton
let attachmentButton: UIButton
var displayAttachmentMenu: () -> Void = { }
var sendMessage: () -> Void = { }
var updateHeight: () -> Void = { }
var text: String {
get {
return self.textInputNode?.attributedText?.string ?? ""
} set(value) {
if let textInputNode = self.textInputNode {
textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(16.0), textColor: UIColor.black)
self.editableTextNodeDidUpdateText(textInputNode)
}
}
}
let textFieldInsets = UIEdgeInsets(top: 9.0, left: 41.0, bottom: 8.0, right: 0.0)
let textInputViewInternalInsets = UIEdgeInsets(top: 4.0, left: 5.0, bottom: 4.0, right: 5.0)
override init() {
self.textInputBackgroundView = UIImageView(image: textInputViewBackground)
self.textPlaceholderNode = TextNode()
self.attachmentButton = UIButton()
self.sendButton = UIButton()
super.init()
self.backgroundColor = UIColor(0xfafafa)
self.attachmentButton.setImage(attachmentIcon, for: [])
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside)
self.view.addSubview(self.attachmentButton)
self.sendButton.titleLabel?.font = Font.medium(17.0)
self.sendButton.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 6.0, bottom: 8.0, right: 6.0)
self.sendButton.setTitleColor(UIColor.blue, for: [])
self.sendButton.setTitleColor(UIColor.gray, for: [.highlighted])
self.sendButton.setTitle("Send", for: [])
self.sendButton.sizeToFit()
self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside)
self.view.addSubview(self.textInputBackgroundView)
let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode)
let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(16.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil)
self.textPlaceholderNode.frame = CGRect(origin: CGPoint(), size: placeholderSize.size)
let _ = placeholderApply()
self.addSubnode(self.textPlaceholderNode)
self.view.addSubview(self.sendButton)
self.textInputBackgroundView.clipsToBounds = true
self.textInputBackgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))))
self.textInputBackgroundView.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func loadTextInputNode() {
let textInputNode = ASEditableTextNode()
textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)]
textInputNode.clipsToBounds = true
textInputNode.delegate = self
self.addSubnode(textInputNode)
self.textInputNode = textInputNode
let sendButtonSize = self.sendButton.bounds.size
textInputNode.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: self.frame.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: self.frame.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)
self.textInputBackgroundView.isUserInteractionEnabled = false
self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0])
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let sendButtonSize = self.sendButton.bounds.size
let textFieldHeight: CGFloat
if let textInputNode = self.textInputNode {
textFieldHeight = min(115.0, max(20.0, ceil(textInputNode.measure(CGSize(width: constrainedSize.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: constrainedSize.height)).height)))
} else {
textFieldHeight = 20.0
}
return CGSize(width: constrainedSize.width, height: textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom)
}
override var frame: CGRect {
get {
return super.frame
} set(value) {
super.frame = value
let sendButtonSize = self.sendButton.bounds.size
let minimalHeight: CGFloat = 45.0
self.sendButton.frame = CGRect(x: value.size.width - sendButtonSize.width, y: value.height - minimalHeight + floor((minimalHeight - sendButtonSize.height) / 2.0), width: sendButtonSize.width, height: sendButtonSize.height)
self.attachmentButton.frame = CGRect(origin: CGPoint(x: 0.0, y: value.height - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))
self.textInputNode?.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: value.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: value.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)
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)
self.textInputBackgroundView.frame = CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: value.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width, height: value.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom)
}
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let textInputNode = self.textInputNode {
self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0
let constrainedSize = CGSize(width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude)
let sendButtonSize = self.sendButton.bounds.size
let textFieldHeight: CGFloat = min(115.0, max(20.0, ceil(textInputNode.measure(CGSize(width: constrainedSize.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: constrainedSize.height)).height)))
if abs(textFieldHeight - textInputNode.frame.size.height) > CGFloat(FLT_EPSILON) {
self.invalidateCalculatedLayout()
self.updateHeight()
}
}
}
@objc func sendButtonPressed() {
let text = self.textInputNode?.attributedText?.string ?? ""
if !text.isEmpty {
self.sendMessage()
}
}
@objc func attachmentButtonPressed() {
self.displayAttachmentMenu()
}
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if self.textInputNode == nil {
self.loadTextInputNode()
}
self.textInputNode?.becomeFirstResponder()
}
}
func animateTextSend() {
/*if let textInputNode = self.textInputNode {
let snapshot = textInputNode.view.snapshotViewAfterScreenUpdates(false)
snapshot.frame = self.textInputBackgroundView.convertRect(textInputNode.view.bounds, fromView: textInputNode.view)
self.textInputBackgroundView.addSubview(snapshot)
UIView.animateWithDuration(0.3, animations: {
snapshot.alpha = 0.0
snapshot.transform = CGAffineTransformMakeTranslation(0.0, -20.0)
}, completion: { _ in
snapshot.removeFromSuperview()
})
}*/
}
/*override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView! {
if let textInputNode = self.textInputNode where self.textInputBackgroundView.frame.contains(point) {
return textInputNode.view
}
return super.hitTest(point, withEvent: event)
}*/
}

View File

@@ -0,0 +1,177 @@
import Foundation
import AsyncDisplayKit
import Postbox
import UIKit
import Display
import TelegramCore
private class ChatListAvatarNodeParameters: NSObject {
let account: Account
let peerId: PeerId
let letters: [String]
let font: UIFont
init(account: Account, peerId: PeerId, letters: [String], font: UIFont) {
self.account = account
self.peerId = peerId
self.letters = letters
self.font = font
super.init()
}
}
let gradientColors: [NSArray] = [
[UIColor(0xff516a).cgColor, UIColor(0xff885e).cgColor],
[UIColor(0xffa85c).cgColor, UIColor(0xffcd6a).cgColor],
[UIColor(0x54cb68).cgColor, UIColor(0xa0de7e).cgColor],
[UIColor(0x2a9ef1).cgColor, UIColor(0x72d5fd).cgColor],
[UIColor(0x665fff).cgColor, UIColor(0x82b1ff).cgColor],
[UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor]
]
private enum ChatListAvatarNodeState: Equatable {
case Empty
case PeerAvatar(Peer)
}
private func ==(lhs: ChatListAvatarNodeState, rhs: ChatListAvatarNodeState) -> Bool {
switch (lhs, rhs) {
case (.Empty, .Empty):
return true
case let (.PeerAvatar(lhsPeer), .PeerAvatar(rhsPeer)) where lhsPeer.isEqual(rhsPeer):
return true
default:
return false
}
}
public final class ChatListAvatarNode: ASDisplayNode {
let font: UIFont
private var parameters: ChatListAvatarNodeParameters?
let imageNode: ImageNode
private var state: ChatListAvatarNodeState = .Empty
public init(font: UIFont) {
self.font = font
self.imageNode = ImageNode()
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.imageNode.isLayerBacked = true
self.addSubnode(self.imageNode)
}
override public var frame: CGRect {
get {
return super.frame
} set(value) {
super.frame = value
self.imageNode.frame = CGRect(origin: CGPoint(), size: value.size)
}
}
public func setPeer(account: Account, peer: Peer) {
let updatedState = ChatListAvatarNodeState.PeerAvatar(peer)
if updatedState != self.state {
self.state = updatedState
let parameters = ChatListAvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font)
self.displaySuspended = true
self.contents = nil
if let signal = peerAvatarImage(account: account, peer: peer) {
self.imageNode.setSignal(signal)
} else {
self.displaySuspended = false
}
if self.parameters == nil || self.parameters != parameters {
self.parameters = parameters
self.setNeedsDisplay()
}
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return parameters ?? NSObject()
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol!, isCancelled: asdisplaynode_iscancelled_block_t, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
bounds.size.height))
context.clip()
let colorIndex: Int
if let parameters = parameters as? ChatListAvatarNodeParameters {
colorIndex = Int(parameters.account.peerId.id + parameters.peerId.id)
} else {
colorIndex = 0
}
let colorsArray: NSArray = gradientColors[colorIndex % gradientColors.count]
var locations: [CGFloat] = [1.0, 0.2];
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
//CGContextDrawRadialGradient(context, gradient, CGPoint(x: bounds.size.width * 0.5, y: -bounds.size.width * 0.2), 0.0, CGPoint(x: bounds.midX, y: bounds.midY), bounds.width, CGGradientDrawingOptions())
context.setBlendMode(.normal)
if let parameters = parameters as? ChatListAvatarNodeParameters {
let letters = parameters.letters
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
let attributedString = NSAttributedString(string: string, attributes: [NSFontAttributeName: parameters.font, NSForegroundColorAttributeName: UIColor.white])
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
/*var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
var leading: CGFloat = 0.0
let lineWidth = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
let opticalBounds = CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: ascent + descent + leading))*/
//let opticalBounds = CTLineGetImageBounds(line, context)
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0))
//let lineOrigin = CGPoint(x: floorToScreenPixels(-opticalBounds.origin.x + (bounds.size.width - opticalBounds.size.width) / 2.0), y: floorToScreenPixels(-opticalBounds.origin.y + (bounds.size.height - opticalBounds.size.height) / 2.0))
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
/*var attributes: [String : AnyObject] = [:]
attributes[NSFontAttributeName] = parameters.font
attributes[NSForegroundColorAttributeName] = UIColor.whiteColor()
let lettersSize = string.sizeWithAttributes(attributes)
string.drawAtPoint(CGPoint(x: floor((bounds.size.width - lettersSize.width) / 2.0), y: floor((bounds.size.height - lettersSize.height) / 2.0)), withAttributes: attributes)*/
}
}
}

View File

@@ -0,0 +1,499 @@
import UIKit
import Postbox
import SwiftSignalKit
import Display
import TelegramCore
enum ChatListMessageViewPosition: Equatable {
case Tail(count: Int)
case Around(index: MessageIndex, anchorIndex: MessageIndex, scrollPosition: ListViewScrollPosition?)
}
func ==(lhs: ChatListMessageViewPosition, rhs: ChatListMessageViewPosition) -> Bool {
switch lhs {
case let .Tail(lhsCount):
switch rhs {
case let .Tail(rhsCount) where lhsCount == rhsCount:
return true
default:
return false
}
case let .Around(lhsId, lhsAnchorIndex, lhsScrollPosition):
switch rhs {
case let .Around(rhsId, rhsAnchorIndex, rhsScrollPosition) where lhsId == rhsId && lhsAnchorIndex == rhsAnchorIndex && lhsScrollPosition == rhsScrollPosition:
return true
default:
return false
}
}
}
private enum ChatListControllerEntryId: Hashable {
case Search
case PeerId(Int64)
var hashValue: Int {
switch self {
case .Search:
return 0
case let .PeerId(peerId):
return peerId.hashValue
}
}
}
private func <(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool {
return lhs.hashValue < rhs.hashValue
}
private func ==(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool {
switch lhs {
case .Search:
switch rhs {
case .Search:
return true
default:
return false
}
case let .PeerId(lhsId):
switch rhs {
case let .PeerId(rhsId):
return lhsId == rhsId
default:
return false
}
}
}
private enum ChatListControllerEntry: Comparable, Identifiable {
case SearchEntry
case MessageEntry(Message, Int)
case HoleEntry(ChatListHole)
case Nothing(MessageIndex)
var index: MessageIndex {
switch self {
case .SearchEntry:
return MessageIndex.absoluteUpperBound()
case let .MessageEntry(message, _):
return MessageIndex(message)
case let .HoleEntry(hole):
return hole.index
case let .Nothing(index):
return index
}
}
var stableId: ChatListControllerEntryId {
switch self {
case .SearchEntry:
return .Search
default:
return .PeerId(self.index.id.peerId.toInt64())
}
}
}
private func <(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool {
return lhs.index < rhs.index
}
private func ==(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool {
switch lhs {
case .SearchEntry:
switch rhs {
case .SearchEntry:
return true
default:
return false
}
case let .MessageEntry(lhsMessage, lhsUnreadCount):
switch rhs {
case let .MessageEntry(rhsMessage, rhsUnreadCount):
return lhsMessage.id == rhsMessage.id && lhsMessage.flags == rhsMessage.flags && lhsUnreadCount == rhsUnreadCount
default:
break
}
case let .HoleEntry(lhsHole):
switch rhs {
case let .HoleEntry(rhsHole):
return lhsHole == rhsHole
default:
return false
}
case let .Nothing(lhsIndex):
switch rhs {
case let .Nothing(rhsIndex):
return lhsIndex == rhsIndex
default:
return false
}
}
return false
}
extension ChatListEntry: Identifiable {
public var stableId: Int64 {
return self.index.id.peerId.toInt64()
}
}
public class ChatListController: ViewController {
let account: Account
private var chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])?
var chatListPosition: ChatListMessageViewPosition?
let chatListDisposable: MetaDisposable = MetaDisposable()
let messageViewQueue = Queue()
let messageViewTransactionQueue = ListViewTransactionQueue()
var settingView = false
let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable()
var chatListDisplayNode: ChatListControllerNode {
get {
return super.displayNode as! ChatListControllerNode
}
}
public init(account: Account) {
self.account = account
super.init()
self.title = "Chats"
self.tabBarItem.title = "Chats"
self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats")
self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected")
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", style: .plain, target: self, action: #selector(self.editPressed))
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Compose, target: self, action: Selector("composePressed"))
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let (view, _) = strongSelf.chatListViewAndEntries, view.laterIndex == nil {
strongSelf.chatListDisplayNode.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, completion: { _ in })
} else {
strongSelf.setMessageViewPosition(.Around(index: MessageIndex.absoluteUpperBound(), anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: .Top), hint: "later", force: true)
}
}
}
self.setMessageViewPosition(.Tail(count: 50), hint: "initial", force: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.chatListDisposable.dispose()
self.openMessageFromSearchDisposable.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ChatListControllerNode(account: self.account)
self.chatListDisplayNode.listView.displayedItemRangeChanged = { [weak self] range in
if let strongSelf = self, !strongSelf.settingView {
if let range = range.loadedRange, let (view, _) = strongSelf.chatListViewAndEntries {
if range.firstIndex < 5 && view.laterIndex != nil {
strongSelf.setMessageViewPosition(.Around(index: view.entries[view.entries.count - 1].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "later", force: false)
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil {
strongSelf.setMessageViewPosition(.Around(index: view.entries[0].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "earlier", force: false)
}
}
}
}
self.chatListDisplayNode.navigationBar = self.navigationBar
self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch()
}
self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in
if let strongSelf = self {
let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in
if modifier.getPeer(peer.id) == nil {
modifier.updatePeers([peer], update: { previousPeer, updatedPeer in
return updatedPeer
})
}
}
strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in
if let strongSelf = strongSelf {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: messageId.peerId, messageId: messageId))
}
}))
}
}
self.chatListDisplayNode.requestOpenPeerFromSearch = { [weak self] peerId in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
}
}
self.displayNodeDidLoad()
}
private func setMessageViewPosition(_ position: ChatListMessageViewPosition, hint: String, force: Bool) {
if self.chatListPosition == nil || self.chatListPosition! != position || force {
let signal: Signal<(ChatListView, ViewUpdateType), NoError>
self.chatListPosition = position
var scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)?
switch position {
case let .Tail(count):
signal = self.account.postbox.tailChatListView(count)
case let .Around(index, _, position):
trace("request around \(index.id.id) \(hint)")
signal = self.account.postbox.aroundChatListView(index, count: 80)
if let position = position {
var directionHint: ListViewScrollToItemDirectionHint = .Up
if let visibleItemRange = self.chatListDisplayNode.listView.displayedItemRange.loadedRange, let (_, entries) = self.chatListViewAndEntries {
if visibleItemRange.firstIndex >= 0 && visibleItemRange.firstIndex < entries.count {
if entries[visibleItemRange.firstIndex].index < index {
directionHint = .Up
} else {
directionHint = .Down
}
}
}
scrollPosition = (index, position, directionHint)
}
}
var firstTime = true
chatListDisposable.set((
signal |> deliverOnMainQueue
).start(next: {[weak self] (view, updateType) in
if let strongSelf = self {
let animated: Bool
switch updateType {
case .Generic:
animated = !firstTime
case .FillHole:
animated = false
case .InitialUnread:
animated = false
case .UpdateVisible:
animated = false
}
strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ?scrollPosition : nil, animated: animated)
firstTime = false
}
}))
}
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
private func chatListControllerEntries(_ view: ChatListView) -> [ChatListControllerEntry] {
var result: [ChatListControllerEntry] = []
for entry in view.entries {
switch entry {
case let .MessageEntry(message, unreadCount):
result.append(.MessageEntry(message, unreadCount))
case let .HoleEntry(hole):
result.append(.HoleEntry(hole))
case let .Nothing(index):
result.append(.Nothing(index))
}
}
if view.laterIndex == nil {
result.append(.SearchEntry)
}
return result
}
private func setPeerView(_ view: ChatListView, firstTime: Bool, scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)?, animated: Bool) {
self.messageViewTransactionQueue.addTransaction { [weak self] completed in
if let strongSelf = self {
strongSelf.settingView = true
let currentEntries = strongSelf.chatListViewAndEntries?.1 ?? []
let viewEntries = strongSelf.chatListControllerEntries(view)
strongSelf.messageViewQueue.async {
//let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: currentEntries, rightList: viewEntries)
let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: currentEntries, rightList: viewEntries)
let updateIndices: [(Int, ChatListControllerEntry)] = []
Queue.mainQueue().async {
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount = currentEntries.count
if deleteIndices.count != 0 {
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
}
let updatedCount = viewEntries.count
var maxAnimatedInsertionIndex = -1
if animated {
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
}
var adjustedIndicesAndItems: [ListViewInsertItem] = []
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
var adjustedPreviousIndex: Int?
if let previousIndex = previousIndex {
adjustedPreviousIndex = previousCount - 1 - previousIndex
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
switch entry {
case .SearchEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in
self?.activateSearch()
}), directionHint: directionHint))
case let .MessageEntry(message, unreadCount):
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListItem(account: strongSelf.account, message: message, unreadCount: unreadCount, action: { [weak self] message in
if let strongSelf = self {
strongSelf.entrySelected(entry)
strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true)
}
}), directionHint: directionHint))
case .HoleEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListHoleItem(), directionHint: directionHint))
case .Nothing:
adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListEmptyItem(), directionHint: directionHint))
}
}
var adjustedUpdateItems: [ListViewUpdateItem] = []
for (index, entry) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let directionHint: ListViewItemOperationDirectionHint? = nil
switch entry {
case .SearchEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: updatedCount - 1 - index, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in
self?.activateSearch()
}), directionHint: directionHint))
case let .MessageEntry(message, unreadCount):
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatListItem(account: strongSelf.account, message: message, unreadCount: unreadCount, action: { [weak self] message in
if let strongSelf = self {
strongSelf.entrySelected(entry)
strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true)
}
}), directionHint: directionHint))
case .HoleEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: updatedCount - 1 - index, item: ChatListHoleItem(), directionHint: directionHint))
case .Nothing:
adjustedUpdateItems.append(ListViewUpdateItem(index: updatedCount - 1 - index, item: ChatListEmptyItem(), directionHint: directionHint))
}
}
if !adjustedDeleteIndices.isEmpty || !adjustedIndicesAndItems.isEmpty || !adjustedUpdateItems.isEmpty || scrollPosition != nil {
var options: ListViewDeleteAndInsertOptions = []
if firstTime {
} else {
let _ = options.insert(.AnimateAlpha)
if animated {
let _ = options.insert(.AnimateInsertion)
}
}
var scrollToItem: ListViewScrollToItem?
if let (itemIndex, itemPosition, directionHint) = scrollPosition {
var index = viewEntries.count - 1
for entry in viewEntries {
if entry.index >= itemIndex {
scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint)
break
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in viewEntries.reversed() {
if entry.index < itemIndex {
scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint)
break
}
index += 1
}
}
}
strongSelf.chatListDisplayNode.listView.deleteAndInsertItems(deleteIndices: adjustedDeleteIndices, insertIndicesAndItems: adjustedIndicesAndItems, updateIndicesAndItems: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.ready.set(single(true, NoError.self))
strongSelf.settingView = false
completed()
}
})
} else {
strongSelf.ready.set(single(true, NoError.self))
strongSelf.settingView = false
completed()
}
strongSelf.chatListViewAndEntries = (view, viewEntries)
}
}
} else {
completed()
}
}
}
private func entrySelected(_ entry: ChatListControllerEntry) {
if case let .MessageEntry(message, _) = entry {
(self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: message.id.peerId))
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
}
@objc func editPressed() {
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
self.chatListDisplayNode.activateSearch()
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch() {
if !self.displayNavigationBar {
self.chatListDisplayNode.deactivateSearch()
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
}
}
}

View File

@@ -0,0 +1,125 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
class ChatListControllerNode: ASDisplayNode {
private let account: Account
let listView: ListView
var navigationBar: NavigationBar?
private var searchDisplayController: SearchDisplayController?
private var containerLayout: (ContainerViewLayout, CGFloat)?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((PeerId) -> Void)?
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
init(account: Account) {
self.account = account
self.listView = ListView()
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.addSubnode(self.listView)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 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
var speedFactor: CGFloat = 1.0
if curve == 7 {
speedFactor = CGFloat(duration) / 0.5
listViewCurve = .Spring(speed: CGFloat(speedFactor))
} else {
listViewCurve = .Default
}
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
self.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in })
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
func activateSearch() {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else {
return
}
var maybePlaceholderNode: SearchBarPlaceholderNode?
self.listView.forEachItemNode { node in
if let node = node as? ChatListSearchItemNode {
maybePlaceholderNode = node.searchBarNode
}
}
if let _ = self.searchDisplayController {
return
}
if let placeholderNode = maybePlaceholderNode {
self.searchDisplayController = SearchDisplayController(contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in
if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch {
requestOpenPeerFromSearch(peerId)
}
}, openMessage: { [weak self] peer, messageId in
if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
requestOpenMessageFromSearch(peer, messageId)
}
}), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()
}
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { subnode in
self.insertSubnode(subnode, belowSubnode: navigationBar)
}, placeholder: placeholderNode)
}
}
func deactivateSearch() {
if let searchDisplayController = self.searchDisplayController {
var maybePlaceholderNode: SearchBarPlaceholderNode?
self.listView.forEachItemNode { node in
if let node = node as? ChatListSearchItemNode {
maybePlaceholderNode = node.searchBarNode
}
}
searchDisplayController.deactivate(placeholder: maybePlaceholderNode)
self.searchDisplayController = nil
}
}
}

View File

@@ -0,0 +1,47 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
class ChatListEmptyItem: ListViewItem {
let selectable: Bool = false
init() {
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatListEmptyItemNode()
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
node.updateItemPosition(first: previousItem == nil, last: nextItem == nil)
completion(node, {})
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
class ChatListEmptyItemNode: ListViewItemNode {
let separatorNode: ASDisplayNode
required init() {
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.separatorNode)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 68.0 - separatorHeight), size: CGSize(width: width, height: separatorHeight))
self.contentSize = CGSize(width: width, height: 68.0)
}
func updateItemPosition(first: Bool, last: Bool) {
self.insets = UIEdgeInsets(top: first ? 4.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
}
}

View File

@@ -0,0 +1,105 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
private let titleFont = Font.regular(17.0)
class ChatListHoleItem: ListViewItem {
let selectable: Bool = false
init() {
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatListHoleItemNode()
node.relativePosition = (first: previousItem == nil, last: nextItem == nil)
node.insets = ChatListItemNode.insets(first: node.relativePosition.first, last: node.relativePosition.last)
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
completion(node, {})
}
}
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? ChatListHoleItemNode {
Queue.mainQueue().async {
let layout = node.asyncLayout()
async {
let first = previousItem == nil
let last = nextItem == nil
let (nodeLayout, apply) = layout(width, first, last)
Queue.mainQueue().async {
completion(nodeLayout, { [weak node] in
apply()
node?.updateBackgroundAndSeparatorsLayout()
})
}
}
}
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
class ChatListHoleItemNode: ListViewItemNode {
let separatorNode: ASDisplayNode
let labelNode: TextNode
var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() {
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
self.separatorNode.isLayerBacked = true
self.labelNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.separatorNode)
self.addSubnode(self.labelNode)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(width, self.relativePosition.first, self.relativePosition.last)
apply()
}
func asyncLayout() -> (_ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let labelNodeLayout = TextNode.asyncLayout(self.labelNode)
return { width, first, last in
let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor(0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil)
let insets = ChatListItemNode.insets(first: first, last: last)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.relativePosition = (first, last)
let _ = labelApply()
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: floor((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 80.0, y: 68.0 - separatorHeight), size: CGSize(width: width - 78.0, height: separatorHeight))
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
strongSelf.updateBackgroundAndSeparatorsLayout()
}
})
}
}
func updateBackgroundAndSeparatorsLayout() {
//let size = self.bounds.size
//let insets = self.insets
}
}

View File

@@ -0,0 +1,409 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
class ChatListItem: ListViewItem {
let account: Account
let message: Message
let unreadCount: Int
let action: (Message) -> Void
let selectable: Bool = true
init(account: Account, message: Message, unreadCount: Int, action: @escaping (Message) -> Void) {
self.account = account
self.message = message
self.unreadCount = unreadCount
self.action = action
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatListItemNode()
node.setupItem(account: self.account, message: self.message, unreadCount: self.unreadCount)
node.relativePosition = (first: previousItem == nil, last: nextItem == nil)
node.insets = ChatListItemNode.insets(first: node.relativePosition.first, last: node.relativePosition.last)
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
completion(node, {})
}
}
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? ChatListItemNode {
Queue.mainQueue().async {
node.setupItem(account: self.account, message: self.message, unreadCount: self.unreadCount)
let layout = node.asyncLayout()
async {
let first = previousItem == nil
let last = nextItem == nil
let (nodeLayout, apply) = layout(self.account, width, first, last)
Queue.mainQueue().async {
completion(nodeLayout, { [weak node] in
apply()
node?.updateBackgroundAndSeparatorsLayout()
})
}
}
}
}
}
func selected() {
self.action(self.message)
}
}
private let titleFont = Font.medium(17.0)
private let textFont = Font.regular(15.0)
private let dateFont = Font.regular(floorToScreenPixels(14.0))
private let badgeFont = Font.regular(14.0)
private func generateStatusCheckImage(single: Bool) -> UIImage? {
return generateImage(CGSize(width: single ? 13.0 : 18.0, height: 13.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0)
//CGContextSetFillColorWithColor(context, UIColor.lightGrayColor().CGColor)
//CGContextFillRect(context, CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: 0.5, y: 0.5)
context.setStrokeColor(UIColor(0x19C700).cgColor)
context.setLineWidth(2.8)
if single {
let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ")
} else {
let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ")
let _ = try? drawSvgPath(context, path: "M13.4492402,16.500967 L15.7523074,18.8031199 L31.4821014,0 ")
}
context.strokePath()
})
}
private func generateBadgeBackgroundImage() -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x1195f2).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
}
private let statusSingleCheckImage = generateStatusCheckImage(single: true)
private let statusDoubleCheckImage = generateStatusCheckImage(single: false)
private let badgeBackgroundImage = generateBadgeBackgroundImage()
private let separatorHeight = 1.0 / UIScreen.main.scale
class ChatListItemNode: ListViewItemNode {
var account: Account?
var message: Message?
var unreadCount: Int = 0
private let highlightedBackgroundNode: ASDisplayNode
let avatarNode: ChatListAvatarNode
let contentNode: ASDisplayNode
let titleNode: TextNode
let textNode: TextNode
let dateNode: TextNode
let statusNode: ASImageNode
let separatorNode: ASDisplayNode
let badgeBackgroundNode: ASImageNode
let badgeTextNode: TextNode
var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(24.0))
self.avatarNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
self.highlightedBackgroundNode.isLayerBacked = true
self.contentNode = ASDisplayNode()
self.contentNode.isLayerBacked = true
self.contentNode.displaysAsynchronously = true
self.contentNode.shouldRasterizeDescendants = true
self.contentNode.isOpaque = true
self.contentNode.backgroundColor = UIColor.white
self.contentNode.contentMode = .left
self.contentNode.contentsScale = UIScreenScale
self.titleNode = TextNode()
self.titleNode.isLayerBacked = true
self.titleNode.displaysAsynchronously = true
self.textNode = TextNode()
self.textNode.isLayerBacked = true
self.textNode.displaysAsynchronously = true
self.dateNode = TextNode()
self.dateNode.isLayerBacked = true
self.dateNode.displaysAsynchronously = true
self.statusNode = ASImageNode()
self.statusNode.isLayerBacked = true
self.statusNode.displaysAsynchronously = false
self.statusNode.displayWithoutProcessing = true
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.isLayerBacked = true
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeBackgroundNode.displayWithoutProcessing = true
self.badgeTextNode = TextNode()
self.badgeTextNode.isLayerBacked = true
self.badgeTextNode.displaysAsynchronously = true
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.separatorNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.titleNode)
self.contentNode.addSubnode(self.textNode)
self.contentNode.addSubnode(self.dateNode)
self.contentNode.addSubnode(self.statusNode)
self.contentNode.addSubnode(self.badgeBackgroundNode)
self.contentNode.addSubnode(self.badgeTextNode)
}
func setupItem(account: Account, message: Message, unreadCount: Int) {
self.account = account
self.message = message
self.unreadCount = unreadCount
let peer = message.peers[message.id.peerId]
if let peer = peer {
self.avatarNode.setPeer(account: account, peer: peer)
}
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(self.account, width, self.relativePosition.first, self.relativePosition.last)
apply()
}
func updateBackgroundAndSeparatorsLayout() {
let size = self.bounds.size
let insets = self.insets
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top - separatorHeight), size: CGSize(width: size.width, height: size.height + separatorHeight))
}
class func insets(first: Bool, last: Bool) -> UIEdgeInsets {
return UIEdgeInsets(top: first ? 4.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
if highlighted {
self.contentNode.displaysAsynchronously = false
self.contentNode.backgroundColor = UIColor.clear
self.contentNode.isOpaque = false
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
strongSelf.contentNode.backgroundColor = UIColor.white
strongSelf.contentNode.isOpaque = true
strongSelf.contentNode.displaysAsynchronously = true
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
self.contentNode.backgroundColor = UIColor.white
self.contentNode.isOpaque = true
self.contentNode.displaysAsynchronously = true
}
}
}
}
func asyncLayout() -> (_ account: Account?, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNode.asyncLayout(self.textNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let message = self.message
let unreadCount = self.unreadCount
return { account, width, first, last in
var textAttributedString: NSAttributedString?
var dateAttributedString: NSAttributedString?
var titleAttributedString: NSAttributedString?
var badgeAttributedString: NSAttributedString?
var statusImage: UIImage?
var currentBadgeBackgroundImage: UIImage?
if let message = message {
let peer = message.peers[message.id.peerId]
var messageText: NSString = message.text as NSString
if message.text.isEmpty {
for media in message.media {
switch media {
case _ as TelegramMediaImage:
messageText = "Photo"
case let fileMedia as TelegramMediaFile:
if fileMedia.isSticker {
messageText = "Sticker"
} else {
messageText = "File"
}
case _ as TelegramMediaMap:
messageText = "Map"
case _ as TelegramMediaContact:
messageText = "Contact"
default:
break
}
}
}
let attributedText: NSAttributedString
if let author = message.author as? TelegramUser, let peer = peer, peer as? TelegramUser == nil {
let peerText: NSString = (author.id == account?.peerId ? "You: " : author.compactDisplayTitle + ": ") as NSString
let mutableAttributedText = NSMutableAttributedString(string: peerText.appending(messageText as String), attributes: [kCTFontAttributeName as String: textFont])
mutableAttributedText.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor.black.cgColor, range: NSMakeRange(0, peerText.length))
mutableAttributedText.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor(0x8e8e93).cgColor, range: NSMakeRange(peerText.length, messageText.length))
attributedText = mutableAttributedText;
} else {
attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: UIColor(0x8e8e93))
}
if let displayTitle = peer?.displayTitle {
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: UIColor.black)
}
textAttributedString = attributedText
var t = Int(message.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(0x8e8e93))
if message.author?.id == account?.peerId {
if !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) {
statusImage = statusDoubleCheckImage
}
}
if unreadCount != 0 {
currentBadgeBackgroundImage = badgeBackgroundImage
badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: UIColor.white)
}
}
let statusWidth = statusImage?.size.width ?? 0.0
let contentRect = CGRect(origin: CGPoint(x: 2.0, y: 12.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0, height: 68.0 - 12.0 - 9.0))
let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: contentRect.width, height: CGFloat.greatestFiniteMagnitude), nil)
let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), nil)
let badgeSize: CGFloat
if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
badgeSize = max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 2.0
} else {
badgeSize = 0.0
}
let (textLayout, textApply) = textLayout(textAttributedString, nil, 1, .end, CGSize(width: contentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), nil)
let titleRect = CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width - dateLayout.size.width - 10.0 - statusWidth, height: contentRect.height))
let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), nil)
let insets = ChatListItemNode.insets(first: first, last: last)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.relativePosition = (first, last)
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 4.0), size: CGSize(width: 60.0, height: 60.0))
strongSelf.contentNode.frame = CGRect(origin: CGPoint(x: 78.0, y: 0.0), size: CGSize(width: width - 78.0, height: 60.0))
let _ = dateApply()
let _ = textApply()
let _ = titleApply()
let _ = badgeApply()
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if let statusImage = statusImage {
strongSelf.statusNode.image = statusImage
strongSelf.statusNode.isHidden = false
let statusSize = statusImage.size
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize)
} else {
strongSelf.statusNode.image = nil
strongSelf.statusNode.isHidden = true
}
if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage
strongSelf.badgeBackgroundNode.isHidden = false
let badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width)
let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height)
let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeLayout.size)
strongSelf.badgeTextNode.frame = badgeTextFrame
strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame
} else {
strongSelf.badgeBackgroundNode.image = nil
strongSelf.badgeBackgroundNode.isHidden = true
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size)
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 78.0 + contentRect.origin.x, y: 68.0 - separatorHeight), size: CGSize(width: width - 78.0, height: separatorHeight))
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
strongSelf.updateBackgroundAndSeparatorsLayout()
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
}

View File

@@ -0,0 +1,126 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private enum ChatListSearchEntry {
case message(Message)
}
final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
private let account: Account
private let openMessage: (Peer, MessageId) -> Void
private let recentPeersNode: ChatListSearchRecentPeersNode
private let listNode: ListView
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
init(account: Account, openPeer: @escaping (PeerId) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) {
self.account = account
self.openMessage = openMessage
self.recentPeersNode = ChatListSearchRecentPeersNode(account: account, peerSelected: openPeer)
self.listNode = ListView()
super.init()
self.backgroundColor = UIColor.white
self.addSubnode(self.recentPeersNode)
self.addSubnode(self.listNode)
self.listNode.isHidden = true
let searchItems = searchQuery.get()
|> mapToSignal { query -> Signal<[ChatListSearchEntry], NoError> in
if let query = query, !query.isEmpty {
return searchMessages(account: account, query: query)
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|> map { messages -> [ChatListSearchEntry] in
return messages.map({ .message($0) })
}
} else {
return .single([])
}
}
let previousSearchItems = Atomic<[ChatListSearchEntry]>(value: [])
self.searchDisposable.set((searchItems
|> deliverOnMainQueue).start(next: { [weak self] items in
if let strongSelf = self {
let previousItems = previousSearchItems.swap(items)
var listItems: [ListViewItem] = []
for item in items {
switch item {
case let .message(message):
listItems.append(ChatListItem(account: account, message: message, unreadCount: 0, action: { [weak strongSelf] _ in
if let strongSelf = strongSelf, let peer = message.peers[message.id.peerId] {
strongSelf.listNode.clearHighlightAnimated(true)
strongSelf.openMessage(peer, message.id)
}
}))
}
}
strongSelf.listNode.deleteAndInsertItems(deleteIndices: (0 ..< previousItems.count).map({ ListViewDeleteItem(index: $0, directionHint: nil) }), insertIndicesAndItems: (0 ..< listItems.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: listItems[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [])
}
}))
}
deinit {
self.searchDisposable.dispose()
}
override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
self.recentPeersNode.isHidden = false
self.listNode.isHidden = true
} else {
self.searchQuery.set(.single(text))
self.recentPeersNode.isHidden = true
self.listNode.isHidden = false
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let recentPeersSize = self.recentPeersNode.measure(CGSize(width: layout.size.width, height: CGFloat.infinity))
self.recentPeersNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: recentPeersSize)
self.recentPeersNode.layout()
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
var speedFactor: CGFloat = 1.0
if curve == 7 {
speedFactor = CGFloat(duration) / 0.5
listViewCurve = .Spring(speed: CGFloat(speedFactor))
} else {
listViewCurve = .Default
}
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, completion: { _ in })
}
}

View File

@@ -0,0 +1,101 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
private let searchBarFont = Font.regular(15.0)
class ChatListSearchItem: ListViewItem {
let selectable: Bool = false
private let placeholder: String
private let activate: () -> Void
init(placeholder: String, activate: @escaping () -> Void) {
self.placeholder = placeholder
self.activate = activate
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatListSearchItemNode()
node.placeholder = self.placeholder
let makeLayout = node.asyncLayout()
let (layout, apply) = makeLayout(width)
node.contentSize = layout.contentSize
node.insets = layout.insets
node.activate = self.activate
completion(node, {
apply()
})
}
}
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? ChatListSearchItemNode {
Queue.mainQueue().async {
let layout = node.asyncLayout()
async {
let (nodeLayout, apply) = layout(width)
Queue.mainQueue().async {
completion(nodeLayout, {
apply()
})
}
}
}
}
}
}
class ChatListSearchItemNode: ListViewItemNode {
let searchBarNode: SearchBarPlaceholderNode
var placeholder: String?
fileprivate var activate: (() -> Void)? {
didSet {
self.searchBarNode.activate = self.activate
}
}
required init() {
self.searchBarNode = SearchBarPlaceholderNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.searchBarNode)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let makeLayout = self.asyncLayout()
let (layout, apply) = makeLayout(width)
apply()
self.contentSize = layout.contentSize
self.insets = layout.insets
}
func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { width in
let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "Search", font: searchBarFont, textColor: UIColor(0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude))
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 44.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: width - 16.0, height: 28.0))
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: width - 16.0, height: 28.0))
}
})
}
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
final class ChatListSearchRecentPeersNode: ASDisplayNode {
private let sectionHeaderNode: ListSectionHeaderNode
private let listView: ListView
private let disposable = MetaDisposable()
init(account: Account, peerSelected: @escaping (PeerId) -> Void) {
self.sectionHeaderNode = ListSectionHeaderNode()
self.sectionHeaderNode.title = "PEOPLE"
self.listView = ListView()
self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
super.init()
self.addSubnode(self.sectionHeaderNode)
self.addSubnode(self.listView)
self.disposable.set((recentPeers(account: account) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self {
var items: [ListViewItem] = []
for peer in peers {
items.append(HorizontalPeerItem(account: account, peer: peer, action: peerSelected))
}
strongSelf.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [])
}
}))
}
deinit {
disposable.dispose()
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 120.0)
}
override func layout() {
super.layout()
let bounds = self.bounds
self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.size.width, height: 29.0))
self.sectionHeaderNode.layout()
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: bounds.size.width)
self.listView.position = CGPoint(x: bounds.size.width / 2.0, y: 92.0 / 2.0 + 29.0)
self.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: bounds.size.width), insets: UIEdgeInsets(), duration: 0.0, curve: .Default), stationaryItemRange: nil, completion: { _ in })
}
}

View File

@@ -0,0 +1,50 @@
import Foundation
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
final class ChatMediaActionSheetController: ActionSheetController {
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
var location: () -> Void = { }
var contacts: () -> Void = { }
override init() {
super.init()
self._ready.set(.single(true))
self.setItemGroups([
ActionSheetItemGroup(items: [
ChatMediaActionSheetRollItem(),
ActionSheetButtonItem(title: "File", action: {}),
ActionSheetButtonItem(title: "Location", action: { [weak self] in
self?.dismissAnimated()
if let location = self?.location {
location()
}
}),
ActionSheetButtonItem(title: "Contact", action: { [weak self] in
self?.dismissAnimated()
if let contacts = self?.contacts {
contacts()
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Cancel", action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -0,0 +1,104 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Photos
import SwiftSignalKit
final class ChatMediaActionSheetRollItem: ActionSheetItem {
func node() -> ActionSheetItemNode {
return ChatMediaActionSheetRollItemNode()
}
}
private final class ChatMediaActionSheetRollItemNode: ActionSheetItemNode, PHPhotoLibraryChangeObserver {
private let listView: ListView
private let label: UILabel
private let button: HighlightTrackingButton
private var assetCollection: PHAssetCollection?
private var fetchResult: PHFetchResult<PHAsset>?
override init() {
self.listView = ListView()
self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
self.label = UILabel()
self.label.backgroundColor = nil
self.label.isOpaque = false
self.label.textColor = UIColor(0x1195f2)
self.label.text = "Photo or Video"
self.label.font = Font.regular(20.0)
self.label.sizeToFit()
self.button = HighlightTrackingButton()
super.init()
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor
})
}
}
}
self.view.addSubview(self.button)
self.view.addSubview(self.label)
self.addSubnode(self.listView)
PHPhotoLibrary.requestAuthorization({ _ in
})
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.fetchResult = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
var items: [ListViewItem] = []
if let fetchResult = self.fetchResult {
for i in 0 ..< fetchResult.count {
let asset = fetchResult.object(at: i)
items.append(ActionSheetRollImageItem(asset: asset))
}
}
if !items.isEmpty {
self.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [])
}
//PHPhotoLibrary.shared().register(self)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 157.0)
}
override func layout() {
super.layout()
let bounds = self.bounds
self.button.frame = CGRect(origin: CGPoint(), size: bounds.size)
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 84.0, height: bounds.size.width)
self.listView.position = CGPoint(x: bounds.size.width / 2.0, y: 84.0 / 2.0 + 8.0)
self.listView.updateSizeAndInsets(size: CGSize(width: 84.0, height: bounds.size.width), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), duration: 0.0, options: UIViewAnimationOptions(rawValue: UInt(0)))
let labelSize = self.label.bounds.size
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.size.width - labelSize.width) / 2.0), y: 84.0 + 16.0 + floorToScreenPixels((bounds.height - 84.0 - 16.0 - labelSize.height) / 2.0)), size: labelSize)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
Queue.concurrentDefaultQueue().async {
//let collectionChanges = changeInstance.changeDetailsForFetchResult(self.fetchResult)
//self.fetchResult = collectionChanges.fetchResultAfterChanges()
}
}
}

View File

@@ -0,0 +1,133 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private func backgroundImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x748391, 0.45).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
}
private let titleFont = UIFont.systemFont(ofSize: 13.0)
class ChatMessageActionItemNode: ChatMessageItemView {
let labelNode: TextNode
let backgroundNode: ASImageNode
private let fetchDisposable = MetaDisposable()
required init() {
self.labelNode = TextNode()
self.labelNode.isLayerBacked = true
self.labelNode.displaysAsynchronously = true
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
super.init(layerBacked: false)
self.backgroundNode.image = backgroundImage(color: UIColor.blue)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.labelNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
override func setupItem(_ item: ChatMessageItem) {
super.setupItem(item)
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
return { item, width, mergedTop, mergedBottom in
var attributedString: NSAttributedString?
for media in item.message.media {
if let action = media as? TelegramMediaAction {
let authorName = item.message.author?.displayTitle ?? ""
switch action.action {
case .groupCreated:
attributedString = NSAttributedString(string: tr(.ChatServiceGroupCreated), font: titleFont, textColor: UIColor.white)
case let .addedMembers(peerIds):
if peerIds.first == item.message.author?.id {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupAddedSelf(authorName)), font: titleFont, textColor: UIColor.white)
} else {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupAddedMembers(authorName, peerDisplayTitles(peerIds, item.message.peers))), font: titleFont, textColor: UIColor.white)
}
case let .removedMembers(peerIds):
if peerIds.first == item.message.author?.id {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedSelf(authorName)), font: titleFont, textColor: UIColor.white)
} else {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedMembers(authorName, peerDisplayTitles(peerIds, item.message.peers))), font: titleFont, textColor: UIColor.white)
}
case let .photoUpdated(image):
if let _ = image {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedPhoto(authorName)), font: titleFont, textColor: UIColor.white)
} else {
attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedPhoto(authorName)), font: titleFont, textColor: UIColor.white)
}
case let .titleUpdated(title):
attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedTitle(authorName, title)), font: titleFont, textColor: UIColor.white)
case .pinnedMessageUpdated:
var replyMessageText = ""
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute, let message = item.message.associatedMessages[attribute.messageId] {
replyMessageText = message.text
}
}
attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedPinnedMessage(authorName, replyMessageText)), font: titleFont, textColor: UIColor.white)
case .joinedByLink:
attributedString = NSAttributedString(string: tr(.ChatServiceGroupJoinedByLink(authorName)), font: titleFont, textColor: UIColor.white)
case .channelMigratedFromGroup, .groupMigratedToChannel:
attributedString = NSAttributedString(string: tr(.ChatServiceGroupMigratedToSupergroup), font: titleFont, textColor: UIColor.white)
default:
attributedString = nil
}
break
}
}
let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil)
let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0)
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] animation in
if let strongSelf = self {
let _ = apply()
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
super.animateInsertion(currentTimestamp, duration: duration)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}

View File

@@ -0,0 +1,54 @@
import Foundation
import Postbox
import Display
import TelegramCore
final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
private let account: Account
private let peerId: PeerId
private let peer: Peer?
private let messageTimestamp: Int32
init(account: Account, peerId: PeerId, peer: Peer?, messageTimestamp: Int32) {
self.account = account
self.peerId = peerId
self.peer = peer
self.messageTimestamp = messageTimestamp
}
func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool {
if case let other as ChatMessageAvatarAccessoryItem = other {
return other.peerId == self.peerId && abs(other.messageTimestamp - self.messageTimestamp) < 5 * 60
}
return false
}
func node() -> ListViewAccessoryItemNode {
let node = ChatMessageAvatarAccessoryItemNode()
node.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
if let peer = self.peer {
node.setPeer(account: account, peer: peer)
}
return node
}
}
final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
let avatarNode: ChatListAvatarNode
override init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0))
self.avatarNode.isLayerBacked = true
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
super.init()
self.isLayerBacked = true
self.addSubnode(self.avatarNode)
}
func setPeer(account: Account, peer: Peer) {
self.avatarNode.setPeer(account: account, peer: peer)
}
}

View File

@@ -0,0 +1,52 @@
func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat) -> ImageCorners {
let topLeftCorner: ImageCorner
let topRightCorner: ImageCorner
switch position.top {
case .Neighbour:
topLeftCorner = .Corner(mergedWithAnotherContentRadius)
topRightCorner = .Corner(mergedWithAnotherContentRadius)
case let .None(mergeStatus):
switch mergeStatus {
case .Left:
topLeftCorner = .Corner(mergedRadius)
topRightCorner = .Corner(normalRadius)
case .None:
topLeftCorner = .Corner(normalRadius)
topRightCorner = .Corner(normalRadius)
case .Right:
topLeftCorner = .Corner(normalRadius)
topRightCorner = .Corner(mergedRadius)
}
}
let bottomLeftCorner: ImageCorner
let bottomRightCorner: ImageCorner
switch position.bottom {
case .Neighbour:
bottomLeftCorner = .Corner(mergedWithAnotherContentRadius)
bottomRightCorner = .Corner(mergedWithAnotherContentRadius)
case let .None(mergeStatus):
switch mergeStatus {
case .Left:
bottomLeftCorner = .Corner(mergedRadius)
bottomRightCorner = .Corner(normalRadius)
case let .None(status):
switch status {
case .Incoming:
bottomLeftCorner = .Tail(normalRadius)
bottomRightCorner = .Corner(normalRadius)
case .Outgoing:
bottomLeftCorner = .Corner(normalRadius)
bottomRightCorner = .Tail(normalRadius)
}
case .Right:
bottomLeftCorner = .Corner(normalRadius)
bottomRightCorner = .Corner(mergedRadius)
}
}
return ImageCorners(topLeft: topLeftCorner, topRight: topRightCorner, bottomLeft: bottomLeftCorner, bottomRight: bottomRightCorner)
}

View File

@@ -0,0 +1,63 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
struct ChatMessageBubbleContentProperties {
let hidesSimpleAuthorHeader: Bool
let headerSpacing: CGFloat
}
enum ChatMessageBubbleNoneMergeStatus {
case Incoming
case Outgoing
}
enum ChatMessageBubbleMergeStatus {
case None(ChatMessageBubbleNoneMergeStatus)
case Left
case Right
}
enum ChatMessageBubbleRelativePosition {
case None(ChatMessageBubbleMergeStatus)
case Neighbour
}
struct ChatMessageBubbleContentPosition {
let top: ChatMessageBubbleRelativePosition
let bottom: ChatMessageBubbleRelativePosition
}
class ChatMessageBubbleContentNode: ASDisplayNode {
var properties: ChatMessageBubbleContentProperties {
return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0)
}
var controllerInteraction: ChatControllerInteraction?
required override init() {
//super.init(layerBacked: false)
super.init()
}
func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
preconditionFailure()
}
func animateInsertion(_ currentTimestamp: Double, duration: Double) {
}
func animateAdded(_ currentTimestamp: Double, duration: Double) {
}
func animateInsertionIntoBubble(_ duration: Double) {
}
func transitionNode(media: Media) -> ASDisplayNode? {
return nil
}
func updateHiddenMedia(_ media: [Media]?) {
}
}

View File

@@ -0,0 +1,660 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
enum ChatMessageBackgroundMergeType {
case None, Top, Bottom, Both
init(top: Bool, bottom: Bool) {
if top && bottom {
self = .Both
} else if top {
self = .Top
} else if bottom {
self = .Bottom
} else {
self = .None
}
}
}
private enum ChatMessageBackgroundType: Equatable {
case Incoming(ChatMessageBackgroundMergeType), Outgoing(ChatMessageBackgroundMergeType)
}
private func ==(lhs: ChatMessageBackgroundType, rhs: ChatMessageBackgroundType) -> Bool {
switch lhs {
case let .Incoming(lhsMergeType):
switch rhs {
case let .Incoming(rhsMergeType):
return lhsMergeType == rhsMergeType
case .Outgoing:
return false
}
case let .Outgoing(lhsMergeType):
switch rhs {
case .Incoming:
return false
case let .Outgoing(rhsMergeType):
return lhsMergeType == rhsMergeType
}
}
}
private let chatMessageBackgroundIncomingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncoming")?.precomposed()
private let chatMessageBackgroundOutgoingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoing")?.precomposed()
private let chatMessageBackgroundIncomingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedTop")?.precomposed()
private let chatMessageBackgroundIncomingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBottom")?.precomposed()
private let chatMessageBackgroundIncomingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBoth")?.precomposed()
private let chatMessageBackgroundOutgoingMergedImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
private let chatMessageBackgroundOutgoingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
private let chatMessageBackgroundOutgoingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
private let chatMessageBackgroundOutgoingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
class ChatMessageBackground: ASImageNode {
private var type: ChatMessageBackgroundType?
override init() {
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = false
self.displayWithoutProcessing = true
}
fileprivate func setType(type: ChatMessageBackgroundType) {
if let currentType = self.type, currentType == type {
return
}
self.type = type
let image: UIImage?
switch type {
case let .Incoming(mergeType):
switch mergeType {
case .None:
image = chatMessageBackgroundIncomingImage
case .Top:
image = chatMessageBackgroundIncomingMergedBottomImage
case .Bottom:
image = chatMessageBackgroundIncomingMergedTopImage
case .Both:
image = chatMessageBackgroundIncomingMergedBothImage
}
case let .Outgoing(mergeType):
switch mergeType {
case .None:
image = chatMessageBackgroundOutgoingImage
case .Top:
image = chatMessageBackgroundOutgoingMergedTopImage
case .Bottom:
image = chatMessageBackgroundOutgoingMergedBottomImage
case .Both:
image = chatMessageBackgroundOutgoingMergedBothImage
}
}
self.image = image
}
}
private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] {
var result: [AnyClass] = []
for media in item.message.media {
if let _ = media as? TelegramMediaImage {
result.append(ChatMessageMediaBubbleContentNode.self)
} else if let file = media as? TelegramMediaFile {
if file.isVideo {
result.append(ChatMessageMediaBubbleContentNode.self)
} else {
result.append(ChatMessageFileBubbleContentNode.self)
}
}
}
if !item.message.text.isEmpty {
result.append(ChatMessageTextBubbleContentNode.self)
}
for media in item.message.media {
if let webpage = media as? TelegramMediaWebpage {
if case .Loaded = webpage.content {
result.append(ChatMessageWebpageBubbleContentNode.self)
}
break
}
}
return result
}
private let nameFont: UIFont = {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: 14.0, weight: UIFontWeightMedium)
} else {
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, 14.0, nil)
}
}()
private let inlineBotPrefixFont = Font.regular(14.0)
private let inlineBotNameFont = nameFont
private let chatMessagePeerIdColors: [UIColor] = [
UIColor(0xfc5c51),
UIColor(0xfa790f),
UIColor(0x0fb297),
UIColor(0x3ca5ec),
UIColor(0x3d72ed),
UIColor(0x895dd5)
]
class ChatMessageBubbleItemNode: ChatMessageItemView {
private let backgroundNode: ChatMessageBackground
private var transitionClippingNode: ASDisplayNode?
private var nameNode: TextNode?
private var forwardInfoNode: ChatMessageForwardInfoNode?
private var replyInfoNode: ChatMessageReplyInfoNode?
private var contentNodes: [ChatMessageBubbleContentNode] = []
private var messageId: MessageId?
private var backgroundFrameTransition: (CGRect, CGRect)?
required init() {
self.backgroundNode = ChatMessageBackground()
super.init(layerBacked: false)
self.addSubnode(self.backgroundNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
super.animateInsertion(currentTimestamp, duration: duration)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
for contentNode in self.contentNodes {
contentNode.animateInsertion(currentTimestamp, duration: duration)
}
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.nameNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.forwardInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.replyInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
for contentNode in self.contentNodes {
contentNode.animateAdded(currentTimestamp, duration: duration)
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
var currentContentClassesPropertiesAndLayouts: [(AnyClass, ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))))] = []
for contentNode in self.contentNodes {
currentContentClassesPropertiesAndLayouts.append((type(of: contentNode) as AnyClass, contentNode.properties, contentNode.asyncLayoutContent()))
}
let authorNameLayout = TextNode.asyncLayout(self.nameNode)
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
let layoutConstants = self.layoutConstants
return { item, width, mergedTop, mergedBottom in
let message = item.message
let incoming = item.account.peerId != message.author?.id
let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroup && item.message.author != nil
let avatarInset: CGFloat = (item.peerId.isGroup && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0
let tmpWidth = width * layoutConstants.bubble.maximumWidthFillFactor
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
var contentPropertiesAndPrepareLayouts: [(ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))))] = []
var addedContentNodes: [ChatMessageBubbleContentNode]?
let contentNodeClasses = contentNodeClassesForItem(item)
for contentNodeClass in contentNodeClasses {
var found = false
for (currentClass, currentProperties, currentLayout) in currentContentClassesPropertiesAndLayouts {
if currentClass == contentNodeClass {
contentPropertiesAndPrepareLayouts.append((currentProperties, currentLayout))
found = true
break
}
}
if !found {
let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init()
contentPropertiesAndPrepareLayouts.append((contentNode.properties, contentNode.asyncLayoutContent()))
if addedContentNodes == nil {
addedContentNodes = [contentNode]
} else {
addedContentNodes!.append(contentNode)
}
}
}
var authorNameString: String?
var inlineBotNameString: String?
var replyMessage: Message?
for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser {
inlineBotNameString = bot.username
} else if let attribute = attribute as? ReplyMessageAttribute {
replyMessage = message.associatedMessages[attribute.messageId]
}
}
var displayHeader = true
if inlineBotNameString == nil && message.forwardInfo == nil && replyMessage == nil {
if let first = contentPropertiesAndPrepareLayouts.first, first.0.hidesSimpleAuthorHeader {
displayHeader = false
}
}
var contentPropertiesAndLayouts: [(ChatMessageBubbleContentProperties, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void)))] = []
let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
if displayHeader {
firstNodeTopPosition = .Neighbour
} else {
firstNodeTopPosition = .None(topNodeMergeStatus)
}
let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus)
var maximumNodeWidth = maximumContentWidth
let contentNodeCount = contentPropertiesAndPrepareLayouts.count
var index = 0
for (properties, prepareLayout) in contentPropertiesAndPrepareLayouts {
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
if index == 0 {
topPosition = firstNodeTopPosition
} else {
topPosition = .Neighbour
}
if index == contentNodeCount - 1 {
bottomPosition = lastNodeTopPosition
} else {
bottomPosition = .Neighbour
}
let (maxNodeWidth, nodeLayout) = prepareLayout(item, layoutConstants, ChatMessageBubbleContentPosition(top: topPosition, bottom: bottomPosition), CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
contentPropertiesAndLayouts.append((properties, nodeLayout))
index += 1
}
var headerSize = CGSize()
var nameNodeOriginY: CGFloat = 0.0
var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil })
var authorNameColor: UIColor?
var replyInfoOriginY: CGFloat = 0.0
var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil })
var forwardInfoOriginY: CGFloat = 0.0
var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil })
if displayHeader {
if let author = message.author, displayAuthorInfo {
authorNameString = author.displayTitle
authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)]
}
if authorNameString != nil || inlineBotNameString != nil {
if headerSize.height < CGFloat(FLT_EPSILON) {
headerSize.height += 4.0
}
let inlineBotNameColor = incoming ? UIColor(0x1195f2) : UIColor(0x00a700)
let attributedString: NSAttributedString
if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString {
let botPrefixString: NSString = " via "
let mutableString = NSMutableAttributedString(string: "\(authorNameString)\(botPrefixString)@\(inlineBotNameString)", attributes: [NSFontAttributeName: inlineBotNameFont, NSForegroundColorAttributeName: inlineBotNameColor])
mutableString.addAttributes([NSFontAttributeName: nameFont, NSForegroundColorAttributeName: authorNameColor], range: NSMakeRange(0, (authorNameString as NSString).length))
mutableString.addAttributes([NSFontAttributeName: inlineBotPrefixFont, NSForegroundColorAttributeName: inlineBotNameColor], range: NSMakeRange((authorNameString as NSString).length, botPrefixString.length))
attributedString = mutableString
} else if let authorNameString = authorNameString, let authorNameColor = authorNameColor {
attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor)
} else if let inlineBotNameString = inlineBotNameString {
attributedString = NSAttributedString(string: "via @\(inlineBotNameString)", font: inlineBotNameFont, textColor: inlineBotNameColor)
} else {
attributedString = NSAttributedString(string: "", font: nameFont, textColor: UIColor.black)
}
let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), nil)
nameNodeSizeApply = (sizeAndApply.0.size, {
return sizeAndApply.1()
})
nameNodeOriginY = headerSize.height
headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
headerSize.height += nameNodeSizeApply.0.height
}
if let forwardInfo = message.forwardInfo {
if headerSize.height < CGFloat(FLT_EPSILON) {
headerSize.height += 4.0
}
let sizeAndApply = forwardInfoLayout(incoming, forwardInfo.source == nil ? forwardInfo.author : forwardInfo.source!, forwardInfo.source == nil ? nil : forwardInfo.author, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
forwardInfoOriginY = headerSize.height
headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
headerSize.height += forwardInfoSizeApply.0.height
}
if let replyMessage = replyMessage {
if headerSize.height < CGFloat(FLT_EPSILON) {
headerSize.height += 6.0
} else {
headerSize.height += 2.0
}
let sizeAndApply = replyInfoLayout(incoming, replyMessage, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
replyInfoOriginY = headerSize.height
headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
headerSize.height += replyInfoSizeApply.0.height + 2.0
}
if headerSize.height > CGFloat(FLT_EPSILON) {
headerSize.height -= 3.0
}
}
var removedContentNodeIndices: [Int]?
findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count {
let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].0
for contentNodeClass in contentNodeClasses {
if currentClass == contentNodeClass {
continue findRemoved
}
}
if removedContentNodeIndices == nil {
removedContentNodeIndices = [i]
} else {
removedContentNodeIndices!.append(i)
}
}
var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, (CGFloat) -> (CGSize, () -> Void))] = []
var maxContentWidth: CGFloat = headerSize.width
for (contentNodeProperties, contentNodeLayout) in contentPropertiesAndLayouts {
let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
maxContentWidth = max(maxContentWidth, contentNodeWidth)
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize))
}
var contentSize = CGSize(width: maxContentWidth, height: 0.0)
index = 0
var contentNodeSizesPropertiesAndApply: [(CGSize, ChatMessageBubbleContentProperties, () -> Void)] = []
for (properties, finalize) in contentNodePropertiesAndFinalize {
let (size, apply) = finalize(maxContentWidth)
contentNodeSizesPropertiesAndApply.append((size, properties, apply))
contentSize.height += size.height
if index == 0 && headerSize.height > CGFloat(FLT_EPSILON) {
contentSize.height += properties.headerSpacing
}
index += 1
}
let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom))
let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize)
let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
let layoutSize = CGSize(width: width, height: layoutBubbleSize.height)
let layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0)
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets)
return (layout, { [weak self] animation in
if let strongSelf = self {
strongSelf.messageId = message.id
if let nameNode = nameNodeSizeApply.1() {
strongSelf.nameNode = nameNode
if nameNode.supernode == nil {
if !nameNode.isNodeLoaded {
nameNode.isLayerBacked = true
}
strongSelf.addSubnode(nameNode)
}
nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0)
} else {
strongSelf.nameNode?.removeFromSupernode()
strongSelf.nameNode = nil
}
if let forwardInfoNode = forwardInfoSizeApply.1() {
strongSelf.forwardInfoNode = forwardInfoNode
if forwardInfoNode.supernode == nil {
strongSelf.addSubnode(forwardInfoNode)
}
forwardInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: forwardInfoSizeApply.0)
} else {
strongSelf.forwardInfoNode?.removeFromSupernode()
strongSelf.forwardInfoNode = nil
}
if let replyInfoNode = replyInfoSizeApply.1() {
strongSelf.replyInfoNode = replyInfoNode
if replyInfoNode.supernode == nil {
strongSelf.addSubnode(replyInfoNode)
}
replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0)
} else {
strongSelf.replyInfoNode?.removeFromSupernode()
strongSelf.replyInfoNode = nil
}
if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 {
var updatedContentNodes = strongSelf.contentNodes
if let removedContentNodeIndices = removedContentNodeIndices {
for index in removedContentNodeIndices.reversed() {
updatedContentNodes[index].removeFromSupernode()
let _ = updatedContentNodes.remove(at: index)
}
}
if let addedContentNodes = addedContentNodes {
for contentNode in addedContentNodes {
updatedContentNodes.append(contentNode)
strongSelf.addSubnode(contentNode)
contentNode.controllerInteraction = strongSelf.controllerInteraction
}
}
strongSelf.contentNodes = updatedContentNodes
}
var contentNodeOrigin = contentOrigin
var contentNodeIndex = 0
for (size, properties, apply) in contentNodeSizesPropertiesAndApply {
apply()
if contentNodeIndex == 0 && headerSize.height > CGFloat(FLT_EPSILON) {
contentNodeOrigin.y += properties.headerSpacing
}
let contentNode = strongSelf.contentNodes[contentNodeIndex]
let contentNodeFrame = CGRect(origin: contentNodeOrigin, size: size)
let previousContentNodeFrame = contentNode.frame
contentNode.frame = contentNodeFrame
if case let .System(duration) = animation {
var animateFrame = false
var animateAlpha = false
if let addedContentNodes = addedContentNodes {
if !addedContentNodes.contains(where: { $0 === contentNode }) {
animateFrame = true
} else {
animateAlpha = true
}
} else {
animateFrame = true
}
if animateFrame {
contentNode.layer.animateFrame(from: previousContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
} else if animateAlpha {
contentNode.animateInsertionIntoBubble(duration)
var previousAlignedContentNodeFrame = contentNodeFrame
previousAlignedContentNodeFrame.origin.x += backgroundFrame.size.width - strongSelf.backgroundNode.frame.size.width
contentNode.layer.animateFrame(from: previousAlignedContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
}
}
contentNodeIndex += 1
contentNodeOrigin.y += size.height
}
let mergeType = ChatMessageBackgroundMergeType(top: mergedBottom, bottom: mergedTop)
if !incoming {
strongSelf.backgroundNode.setType(type: .Outgoing(mergeType))
} else {
strongSelf.backgroundNode.setType(type: .Incoming(mergeType))
}
if case .System = animation {
strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
strongSelf.enableTransitionClippingNode()
} else {
if let _ = strongSelf.backgroundFrameTransition {
strongSelf.animateFrameTransition(1.0)
strongSelf.backgroundFrameTransition = nil
}
strongSelf.backgroundNode.frame = backgroundFrame
strongSelf.disableTransitionClippingNode()
}
}
})
}
}
private func addContentNode(node: ChatMessageBubbleContentNode) {
if let transitionClippingNode = self.transitionClippingNode {
transitionClippingNode.addSubnode(node)
} else {
self.addSubnode(node)
}
}
private func enableTransitionClippingNode() {
if self.transitionClippingNode == nil {
let node = ASDisplayNode()
node.clipsToBounds = true
var backgroundFrame = self.backgroundNode.frame
backgroundFrame = backgroundFrame.insetBy(dx: 0.0, dy: 1.0)
node.frame = backgroundFrame
node.bounds = CGRect(origin: CGPoint(x: backgroundFrame.origin.x, y: backgroundFrame.origin.y), size: backgroundFrame.size)
for contentNode in self.contentNodes {
node.addSubnode(contentNode)
}
self.addSubnode(node)
self.transitionClippingNode = node
}
}
private func disableTransitionClippingNode() {
if let transitionClippingNode = self.transitionClippingNode {
for contentNode in self.contentNodes {
self.addSubnode(contentNode)
}
transitionClippingNode.removeFromSupernode()
self.transitionClippingNode = nil
}
}
override func animateFrameTransition(_ progress: CGFloat) {
super.animateFrameTransition(progress)
if let backgroundFrameTransition = self.backgroundFrameTransition {
let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect
self.backgroundNode.frame = backgroundFrame
if let transitionClippingNode = self.transitionClippingNode {
var fixedBackgroundFrame = backgroundFrame
fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: 1.0)
transitionClippingNode.frame = fixedBackgroundFrame
transitionClippingNode.bounds = CGRect(origin: CGPoint(x: fixedBackgroundFrame.origin.x, y: fixedBackgroundFrame.origin.y), size: fixedBackgroundFrame.size)
if progress >= 1.0 - CGFloat(FLT_EPSILON) {
self.disableTransitionClippingNode()
}
}
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
let location = recognizer.location(in: self.view)
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
self.controllerInteraction?.testNavigateToMessage(item.message.id, attribute.messageId)
break
}
}
}
//self.controllerInteraction?.testNavigateToMessage(messageId)
}
default:
break
}
}
override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? {
if let item = self.item, item.message.id == id {
for contentNode in self.contentNodes {
if let result = contentNode.transitionNode(media: media) {
return result
}
}
}
return nil
}
override func updateHiddenMedia() {
if let item = self.item, let controllerInteraction = self.controllerInteraction {
for contentNode in self.contentNodes {
contentNode.updateHiddenMedia(controllerInteraction.hiddenMedia[item.message.id])
}
}
}
}

View File

@@ -0,0 +1,284 @@
import Foundation
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
private let dateFont = UIFont.italicSystemFont(ofSize: 11.0)
private func generateCheckImage(partial: Bool) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: 0.5, y: 0.5)
context.setStrokeColor(UIColor(0x19C700).cgColor)
context.setLineWidth(2.5)
if partial {
let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ")
} else {
let _ = try? drawSvgPath(context, path: "M1,10 L7,16 L20.9985125,1 ")
}
context.strokePath()
})
}
private func generateClockFrameImage() -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0x42b649).cgColor)
context.setFillColor(UIColor(0x42b649).cgColor)
let strokeWidth: CGFloat = 1.0
context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth))
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0))
})
}
private func generateClockMinImage() -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x42b649).cgColor)
let strokeWidth: CGFloat = 1.0
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth))
})
}
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
return
}
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = duration
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(M_PI * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
layer.add(basicAnimation, forKey: "clockFrameAnimation")
}
private let checkFullImage = generateCheckImage(partial: false)
private let checkPartialImage = generateCheckImage(partial: true)
private let incomingDateColor = UIColor(0x525252, 0.6)
private let outgoingDateColor = UIColor(0x008c09, 0.8)
private let clockFrameImage = generateClockFrameImage()
private let clockMinImage = generateClockMinImage()
enum ChatMessageDateAndStatusOutgoingType {
case Sent(read: Bool)
case Sending
case Failed
}
enum ChatMessageDateAndStatusType {
case BubbleIncoming
case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType)
}
class ChatMessageDateAndStatusNode: ASTransformLayerNode {
private var checkSentNode: ASImageNode?
private var checkReadNode: ASImageNode?
private var clockFrameNode: ASImageNode?
private var clockMinNode: ASImageNode?
private let dateNode: TextNode
override init() {
self.dateNode = TextNode()
self.dateNode.isLayerBacked = true
self.dateNode.displaysAsynchronously = true
super.init()
self.addSubnode(self.dateNode)
}
func asyncLayout() -> (_ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, () -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
var checkReadNode = self.checkReadNode
var checkSentNode = self.checkSentNode
var clockFrameNode = self.clockFrameNode
var clockMinNode = self.clockMinNode
return { dateText, type, constrainedSize in
let dateColor: UIColor
var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
switch type {
case .BubbleIncoming:
dateColor = incomingDateColor
case let .BubbleOutgoing(status):
dateColor = outgoingDateColor
outgoingStatus = status
}
let (date, dateApply) = dateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, nil)
let leftInset: CGFloat = 10.0
let statusWidth: CGFloat
var checkSentFrame: CGRect?
var checkReadFrame: CGRect?
var clockPosition = CGPoint()
let loadedCheckFullImage = checkFullImage
let loadedCheckPartialImage = checkPartialImage
if let outgoingStatus = outgoingStatus {
switch outgoingStatus {
case .Sending:
statusWidth = 13.0
if checkReadNode == nil {
checkReadNode = ASImageNode()
checkReadNode?.isLayerBacked = true
checkReadNode?.displaysAsynchronously = false
checkReadNode?.displayWithoutProcessing = true
}
if checkSentNode == nil {
checkSentNode = ASImageNode()
checkSentNode?.isLayerBacked = true
checkSentNode?.displaysAsynchronously = false
checkSentNode?.displayWithoutProcessing = true
}
if clockFrameNode == nil {
clockFrameNode = ASImageNode()
clockFrameNode?.isLayerBacked = true
clockFrameNode?.displaysAsynchronously = false
clockFrameNode?.displayWithoutProcessing = true
clockFrameNode?.image = clockFrameImage
clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize())
}
if clockMinNode == nil {
clockMinNode = ASImageNode()
clockMinNode?.isLayerBacked = true
clockMinNode?.displaysAsynchronously = false
clockMinNode?.displayWithoutProcessing = true
clockMinNode?.image = clockMinImage
clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize())
}
clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5)
case let .Sent(read):
statusWidth = 13.0
if checkReadNode == nil {
checkReadNode = ASImageNode()
checkReadNode?.isLayerBacked = true
checkReadNode?.displaysAsynchronously = false
checkReadNode?.displayWithoutProcessing = true
}
if checkSentNode == nil {
checkSentNode = ASImageNode()
checkSentNode?.isLayerBacked = true
checkSentNode?.displaysAsynchronously = false
checkSentNode?.displayWithoutProcessing = true
}
clockFrameNode = nil
clockMinNode = nil
let checkSize = checkFullImage!.size
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize)
if read {
checkReadFrame = CGRect(origin: CGPoint(x: checkSentFrame!.origin.x - 6.0, y: checkSentFrame!.origin.y), size: checkSize)
}
case .Failed:
statusWidth = 0.0
checkReadNode = nil
checkSentNode = nil
clockFrameNode = nil
clockMinNode = nil
}
} else {
statusWidth = 0.0
checkReadNode = nil
checkSentNode = nil
clockFrameNode = nil
clockMinNode = nil
}
return (CGSize(width: leftInset + date.size.width + statusWidth, height: date.size.height), { [weak self] in
if let strongSelf = self {
let _ = dateApply()
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: date.size)
if let clockFrameNode = clockFrameNode {
if strongSelf.clockFrameNode == nil {
strongSelf.clockFrameNode = clockFrameNode
strongSelf.addSubnode(clockFrameNode)
}
clockFrameNode.position = clockPosition
if let clockFrameNode = strongSelf.clockFrameNode {
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
}
} else if let clockFrameNode = strongSelf.clockFrameNode {
clockFrameNode.removeFromSupernode()
strongSelf.clockFrameNode = nil
}
if let clockMinNode = clockMinNode {
if strongSelf.clockMinNode == nil {
strongSelf.clockMinNode = clockMinNode
strongSelf.addSubnode(clockMinNode)
}
clockMinNode.position = clockPosition
if let clockMinNode = strongSelf.clockMinNode {
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
}
} else if let clockMinNode = strongSelf.clockMinNode {
clockMinNode.removeFromSupernode()
strongSelf.clockMinNode = nil
}
if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode {
if strongSelf.checkSentNode == nil {
strongSelf.checkSentNode = checkSentNode
strongSelf.addSubnode(checkSentNode)
}
checkSentNode.image = loadedCheckPartialImage
if let checkSentFrame = checkSentFrame {
checkSentNode.isHidden = false
checkSentNode.frame = checkSentFrame
} else {
checkSentNode.isHidden = true
}
if strongSelf.checkReadNode == nil {
strongSelf.checkReadNode = checkReadNode
strongSelf.addSubnode(checkReadNode)
}
checkReadNode.image = loadedCheckFullImage
if let checkReadFrame = checkReadFrame {
checkReadNode.isHidden = false
checkReadNode.frame = checkReadFrame
} else {
checkReadNode.isHidden = true
}
} else if let checkSentNode = strongSelf.checkSentNode, let checkReadNode = strongSelf.checkReadNode {
checkSentNode.removeFromSupernode()
checkReadNode.removeFromSupernode()
strongSelf.checkSentNode = nil
strongSelf.checkReadNode = nil
}
}
})
}
}
}

View File

@@ -0,0 +1,73 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
private let interactiveFileNode: ChatMessageInteractiveFileNode
private var item: ChatMessageItem?
required init() {
self.interactiveFileNode = ChatMessageInteractiveFileNode()
super.init()
self.addSubnode(self.interactiveFileNode)
self.interactiveFileNode.activateLocalContent = { [weak self] in
if let strongSelf = self {
if let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction {
controllerInteraction.openMessage(item.message.id)
}
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
return { item, layoutConstants, position, constrainedSize in
var selectedFile: TelegramMediaFile?
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
selectedFile = telegramFile
}
}
let (initialWidth, refineLayout) = interactiveFileLayout(item.account, selectedFile!, item.message.flags.contains(.Incoming), CGSize(width: constrainedSize.width, height: constrainedSize.height))
return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in
let (refinedWidth, finishLayout) = refineLayout(constrainedSize)
return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + layoutConstants.file.bubbleInsets.bottom), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
fileApply()
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}

View File

@@ -0,0 +1,53 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
private let prefixFont = Font.regular(13.0)
private let peerFont = Font.medium(13.0)
class ChatMessageForwardInfoNode: ASTransformLayerNode {
private var textNode: TextNode?
override init() {
super.init()
}
class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ incoming: Bool, _ peer: Peer, _ authorPeer: Peer?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) {
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
return { incoming, peer, authorPeer, constrainedSize in
let prefix: NSString = "Forwarded Message\nFrom: "
let peerString: String
if let authorPeer = authorPeer {
peerString = "\(peer.displayTitle) (\(authorPeer.displayTitle))"
} else {
peerString = peer.displayTitle
}
let completeString: NSString = "\(prefix)\(peerString)" as NSString
let color = incoming ? UIColor(0x007bff) : UIColor(0x00a516)
let string = NSMutableAttributedString(string: completeString as String, attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: prefixFont])
string.addAttributes([NSFontAttributeName: peerFont], range: NSMakeRange(prefix.length, completeString.length - prefix.length))
let (textLayout, textApply) = textNodeLayout(string, nil, 2, .end, constrainedSize, nil)
return (textLayout.size, {
let node: ChatMessageForwardInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageForwardInfoNode()
}
let textNode = textApply()
if node.textNode == nil {
textNode.isLayerBacked = true
node.textNode = textNode
node.addSubnode(textNode)
}
textNode.frame = CGRect(origin: CGPoint(), size: textLayout.size)
return node
})
}
}
}

View File

@@ -0,0 +1,247 @@
import Foundation
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import Display
import TelegramCore
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
private let titleFont = Font.regular(16.0)
private let descriptionFont = Font.regular(13.0)
private let incomingTitleColor = UIColor(0x0b8bed)
private let outgoingTitleColor = UIColor(0x3faa3c)
private let incomingDescriptionColor = UIColor(0x999999)
private let outgoingDescriptionColor = UIColor(0x6fb26a)
private let fileIconIncomingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming")?.precomposed()
private let fileIconOutgoingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentOutgoing")?.precomposed()
final class ChatMessageInteractiveFileNode: ASTransformNode {
private let titleNode: TextNode
private let descriptionNode: TextNode
private var iconNode: TransformImageNode?
private var progressNode: RadialProgressNode?
private var tapRecognizer: UITapGestureRecognizer?
private let statusDisposable = MetaDisposable()
private let fetchControls = Atomic<FetchControls?>(value: nil)
private var fetchStatus: MediaResourceStatus?
private let fetchDisposable = MetaDisposable()
var activateLocalContent: () -> Void = { }
private var file: TelegramMediaFile?
init() {
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = true
self.titleNode.isLayerBacked = true
self.descriptionNode = TextNode()
self.descriptionNode.displaysAsynchronously = true
self.descriptionNode.isLayerBacked = true
super.init(layerBacked: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.descriptionNode)
}
deinit {
self.statusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.fileTap(_:)))
self.view.addGestureRecognizer(tapRecognizer)
self.tapRecognizer = tapRecognizer
}
@objc func progressPressed() {
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Fetching:
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
cancel()
}
case .Remote:
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
fetch()
}
case .Local:
break
}
}
}
@objc func fileTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
self.activateLocalContent()
} else {
self.activateLocalContent()
//self.progressPressed()
}
}
}
func asyncLayout() -> (_ account: Account, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let currentFile = self.file
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
let descriptionAsyncLayout = TextNode.asyncLayout(self.descriptionNode)
return { account, file, incoming, constrainedSize in
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
//var updateImageSignal: Signal<TransformImageArguments -> DrawingContext, NoError>?
var updatedStatusSignal: Signal<MediaResourceStatus, NoError>?
var updatedFetchControls: FetchControls?
var mediaUpdated = false
if let currentFile = currentFile {
mediaUpdated = file != currentFile
} else {
mediaUpdated = true
}
if mediaUpdated {
//updateImageSignal = chatMessagePhoto(account, photo: image)
updatedStatusSignal = chatMessageFileStatus(account: account, file: file)
updatedFetchControls = FetchControls(fetch: { [weak self] in
if let strongSelf = self {
strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start())
}
}, cancel: {
chatMessageFileCancelInteractiveFetch(account: account, file: file)
})
}
var candidateTitleString: NSAttributedString?
var candidateDescriptionString: NSAttributedString?
for attribute in file.attributes {
if case let .Audio(_, _, title, performer, _) = attribute {
candidateTitleString = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor)
candidateDescriptionString = NSAttributedString(string: performer ?? dataSizeString(file.size), font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor)
break
}
}
var titleString: NSAttributedString
let descriptionString: NSAttributedString
if let candidateTitleString = candidateTitleString {
titleString = candidateTitleString
} else {
titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor)
}
if let candidateDescriptionString = candidateDescriptionString {
descriptionString = candidateDescriptionString
} else {
descriptionString = NSAttributedString(string: dataSizeString(file.size), font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor)
}
let textConstrainedSize = CGSize(width: constrainedSize.width - 44.0 - 8.0, height: constrainedSize.height)
let (titleLayout, titleApply) = titleAsyncLayout(titleString, nil, 1, .middle, textConstrainedSize, nil)
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(descriptionString, nil, 1, .middle, textConstrainedSize, nil)
return (max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0, { boundingWidth in
let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 44.0, height: 44.0))
let titleAndDescriptionHeight = titleLayout.size.height - 1.0 + descriptionLayout.size.height
let titleFrame = CGRect(origin: CGPoint(x: progressFrame.maxX + 8.0, y: floor((44.0 - titleAndDescriptionHeight) / 2.0)), size: titleLayout.size)
let descriptionFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY - 1.0), size: descriptionLayout.size)
return (titleFrame.union(descriptionFrame).union(progressFrame).size, { [weak self] in
if let strongSelf = self {
strongSelf.file = file
let _ = titleApply()
let _ = descriptionApply()
strongSelf.titleNode.frame = titleFrame
strongSelf.descriptionNode.frame = descriptionFrame
/*if let updateImageSignal = updateImageSignal {
strongSelf.imageNode.setSignal(account, signal: updateImageSignal)
}*/
if let updatedStatusSignal = updatedStatusSignal {
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf {
strongSelf.fetchStatus = status
if strongSelf.progressNode == nil {
let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(incoming ? 0x1195f2 : 0x3fc33b), foregroundColor: incoming ? UIColor.white : UIColor(0xe1ffc7), icon: incoming ? fileIconIncomingImage : fileIconOutgoingImage))
strongSelf.progressNode = progressNode
progressNode.frame = progressFrame
strongSelf.addSubnode(progressNode)
}
switch status {
case let .Fetching(progress):
strongSelf.progressNode?.state = .Fetching(progress: progress)
case .Local:
strongSelf.progressNode?.state = .Play
case .Remote:
strongSelf.progressNode?.state = .Remote
}
}
}
}))
}
strongSelf.progressNode?.frame = progressFrame
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
}
}
})
})
})
}
}
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) {
let currentAsyncLayout = node?.asyncLayout()
return { account, file, incoming, constrainedSize in
var fileNode: ChatMessageInteractiveFileNode
var fileLayout: (_ account: Account, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout {
fileNode = node
fileLayout = currentAsyncLayout
} else {
fileNode = ChatMessageInteractiveFileNode()
fileLayout = fileNode.asyncLayout()
}
let (initialWidth, continueLayout) = fileLayout(account, file, incoming, constrainedSize)
return (initialWidth, { constrainedSize in
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
return (finalWidth, { boundingWidth in
let (finalSize, apply) = finalLayout(boundingWidth)
return (finalSize, {
apply()
return fileNode
})
})
})
}
}
}

View File

@@ -0,0 +1,260 @@
import Foundation
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import Display
import TelegramCore
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
final class ChatMessageInteractiveMediaNode: ASTransformNode {
private let imageNode: TransformImageNode
private var progressNode: RadialProgressNode?
private var tapRecognizer: UITapGestureRecognizer?
private var media: Media?
private let statusDisposable = MetaDisposable()
private let fetchControls = Atomic<FetchControls?>(value: nil)
private var fetchStatus: MediaResourceStatus?
private let fetchDisposable = MetaDisposable()
var activateLocalContent: () -> Void = { }
init() {
self.imageNode = TransformImageNode()
super.init(layerBacked: false)
self.imageNode.displaysAsynchronously = false
self.addSubnode(self.imageNode)
}
deinit {
self.statusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
self.imageNode.view.addGestureRecognizer(tapRecognizer)
self.tapRecognizer = tapRecognizer
}
@objc func progressPressed() {
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Fetching:
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
cancel()
}
case .Remote:
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
fetch()
}
case .Local:
break
}
}
}
@objc func imageTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) {
self.activateLocalContent()
} else {
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
self.activateLocalContent()
} else {
self.progressPressed()
}
}
}
}
func asyncLayout() -> (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let currentMedia = self.media
let imageLayout = self.imageNode.asyncLayout()
return { account, media, corners, automaticDownload, constrainedSize in
var initialBoundingSize: CGSize
var nativeSize: CGSize
if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
initialBoundingSize = dimensions.fitted(CGSize(width: min(200.0, constrainedSize.width - 60.0), height: 200.0))
nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize)
} else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions {
initialBoundingSize = dimensions.fitted(CGSize(width: min(200.0, constrainedSize.width - 60.0), height: 200.0))
nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize)
} else {
initialBoundingSize = CGSize(width: 32.0, height: 32.0)
nativeSize = initialBoundingSize
}
initialBoundingSize.width = max(initialBoundingSize.width, 60.0)
initialBoundingSize.height = max(initialBoundingSize.height, 60.0)
nativeSize.width = max(nativeSize.width, 60.0)
nativeSize.height = max(nativeSize.height, 60.0)
return (nativeSize.width, { constrainedSize in
let boundingSize = initialBoundingSize.fitted(constrainedSize)
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext, NoError>?
var updatedStatusSignal: Signal<MediaResourceStatus, NoError>?
var updatedFetchControls: FetchControls?
var mediaUpdated = false
if let currentMedia = currentMedia {
mediaUpdated = !media.isEqual(currentMedia)
} else {
mediaUpdated = true
}
if mediaUpdated {
if let image = media as? TelegramMediaImage {
updateImageSignal = chatMessagePhoto(account: account, photo: image)
updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image)
updatedFetchControls = FetchControls(fetch: { [weak self] in
if let strongSelf = self {
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
}
}, cancel: {
chatMessagePhotoCancelInteractiveFetch(account: account, photo: image)
})
} else if let file = media as? TelegramMediaFile {
updateImageSignal = chatMessageVideo(account: account, video: file)
updatedStatusSignal = chatMessageFileStatus(account: account, file: file)
updatedFetchControls = FetchControls(fetch: { [weak self] in
if let strongSelf = self {
strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start())
}
}, cancel: {
chatMessageFileCancelInteractiveFetch(account: account, file: file)
})
}
}
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)
return (boundingSize.width, { boundingWidth in
let adjustedWidth = boundingWidth
let adjustedHeight = boundingSize.aspectFitted(CGSize(width: adjustedWidth, height: CGFloat.greatestFiniteMagnitude)).height
let adjustedImageSize = CGSize(width: adjustedWidth, height: min(adjustedHeight, floorToScreenPixels(boundingSize.height * 1.4)))
let adjustedArguments = TransformImageArguments(corners: corners, imageSize: nativeSize, boundingSize: adjustedImageSize, intrinsicInsets: UIEdgeInsets())
let adjustedImageFrame = CGRect(origin: imageFrame.origin, size: adjustedArguments.drawingSize)
let imageApply = imageLayout(adjustedArguments)
return (CGSize(width: adjustedImageSize.width, height: adjustedImageSize.height), { [weak self] in
if let strongSelf = self {
strongSelf.media = media
strongSelf.imageNode.frame = adjustedImageFrame
strongSelf.progressNode?.position = CGPoint(x: adjustedImageFrame.midX, y: adjustedImageFrame.midY)
if let updateImageSignal = updateImageSignal {
strongSelf.imageNode.setSignal(account: account, signal: updateImageSignal)
}
if let updatedStatusSignal = updatedStatusSignal {
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf {
strongSelf.fetchStatus = status
if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) {
if let progressNode = strongSelf.progressNode {
progressNode.removeFromSupernode()
strongSelf.progressNode = nil
}
} else {
if case .Local = status {
if let progressNode = strongSelf.progressNode {
progressNode.removeFromSupernode()
strongSelf.progressNode = nil
}
} else {
if strongSelf.progressNode == nil {
let progressNode = RadialProgressNode()
progressNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0))
progressNode.position = strongSelf.imageNode.position
strongSelf.progressNode = progressNode
strongSelf.addSubnode(progressNode)
}
}
switch status {
case let .Fetching(progress):
strongSelf.progressNode?.state = .Fetching(progress: progress)
case .Local:
var state: RadialProgressState = .None
if let file = media as? TelegramMediaFile {
if file.isVideo {
state = .Play
}
}
strongSelf.progressNode?.state = state
case .Remote:
strongSelf.progressNode?.state = .Remote
}
}
}
}
}))
}
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
if automaticDownload {
if let image = media as? TelegramMediaImage {
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
}
}
}
imageApply()
}
})
})
})
}
}
static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) {
let currentAsyncLayout = node?.asyncLayout()
return { account, media, corners, automaticDownload, constrainedSize in
var imageNode: ChatMessageInteractiveMediaNode
var imageLayout: (_ account: Account, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout {
imageNode = node
imageLayout = currentAsyncLayout
} else {
imageNode = ChatMessageInteractiveMediaNode()
imageLayout = imageNode.asyncLayout()
}
let (initialWidth, continueLayout) = imageLayout(account, media, corners, automaticDownload, constrainedSize)
return (initialWidth, { constrainedSize in
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
return (finalWidth, { boundingWidth in
let (finalSize, apply) = finalLayout(boundingWidth)
return (finalSize, {
apply()
return imageNode
})
})
})
}
}
}

View File

@@ -0,0 +1,147 @@
import Foundation
import UIKit
import Postbox
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
private func mediaIsNotMergeable(_ media: Media) -> Bool {
if let file = media as? TelegramMediaFile, file.isSticker {
return true
}
if let _ = media as? TelegramMediaAction {
return true
}
return false
}
private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool {
if abs(lhs.timestamp - rhs.timestamp) < 5 * 60 && lhs.author?.id == rhs.author?.id {
for media in lhs.media {
if mediaIsNotMergeable(media) {
return false
}
}
for media in rhs.media {
if mediaIsNotMergeable(media) {
return false
}
}
return true
}
return false
}
public class ChatMessageItem: ListViewItem, CustomStringConvertible {
let account: Account
let peerId: PeerId
let controllerInteraction: ChatControllerInteraction
let message: Message
public let accessoryItem: ListViewAccessoryItem?
public init(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message) {
self.account = account
self.peerId = peerId
self.controllerInteraction = controllerInteraction
self.message = message
var accessoryItem: ListViewAccessoryItem?
let incoming = account.peerId != message.author?.id
let displayAuthorInfo = incoming && message.author != nil && peerId.isGroup
if displayAuthorInfo {
var hasActionMedia = false
for media in message.media {
if media is TelegramMediaAction {
hasActionMedia = true
break
}
}
if !hasActionMedia {
if let author = message.author {
accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: author.id, peer: author, messageTimestamp: message.timestamp)
}
}
}
self.accessoryItem = accessoryItem
}
public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
var viewClassName: AnyClass = ChatMessageBubbleItemNode.self
for media in message.media {
if let telegramFile = media as? TelegramMediaFile, telegramFile.isSticker {
viewClassName = ChatMessageStickerItemNode.self
} else if let _ = media as? TelegramMediaAction {
viewClassName = ChatMessageActionItemNode.self
}
}
let configure = { () -> Void in
let node = (viewClassName as! ChatMessageItemView.Type).init()
node.controllerInteraction = self.controllerInteraction
node.setupItem(self)
let nodeLayout = node.asyncLayout()
let (top, bottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, width, top, bottom)
node.contentSize = layout.contentSize
node.insets = layout.insets
completion(node, {
apply(.None)
})
}
if Thread.isMainThread {
async {
configure()
}
} else {
configure()
}
}
final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: Bool, bottom: Bool) {
var mergedTop = false
var mergedBottom = false
if let top = top as? ChatMessageItem {
mergedBottom = messagesShouldBeMerged(message, top.message)
}
if let bottom = bottom as? ChatMessageItem {
mergedTop = messagesShouldBeMerged(message, bottom.message)
}
return (mergedTop, mergedBottom)
}
public 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? ChatMessageItemView {
Queue.mainQueue().async {
node.setupItem(self)
let nodeLayout = node.asyncLayout()
async {
let (top, bottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, width, top, bottom)
Queue.mainQueue().async {
completion(layout, {
apply(animation)
})
}
}
}
}
}
public var description: String {
return "(ChatMessageItem id: \(self.message.id), text: \"\(self.message.text)\")"
}
}

View File

@@ -0,0 +1,120 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
struct ChatMessageItemBubbleLayoutConstants {
let edgeInset: CGFloat
let defaultSpacing: CGFloat
let mergedSpacing: CGFloat
let maximumWidthFillFactor: CGFloat
let minimumSize: CGSize
let contentInsets: UIEdgeInsets
}
struct ChatMessageItemTextLayoutConstants {
let bubbleInsets: UIEdgeInsets
}
struct ChatMessageItemImageLayoutConstants {
let bubbleInsets: UIEdgeInsets
let defaultCornerRadius: CGFloat
let mergedCornerRadius: CGFloat
let contentMergedCornerRadius: CGFloat
}
struct ChatMessageItemFileLayoutConstants {
let bubbleInsets: UIEdgeInsets
}
struct ChatMessageItemLayoutConstants {
let avatarDiameter: CGFloat
let bubble: ChatMessageItemBubbleLayoutConstants
let image: ChatMessageItemImageLayoutConstants
let text: ChatMessageItemTextLayoutConstants
let file: ChatMessageItemFileLayoutConstants
init() {
self.avatarDiameter = 37.0
self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.5, mergedSpacing: 0.0, maximumWidthFillFactor: 0.9, minimumSize: CGSize(width: 40.0, height: 33.0), contentInsets: UIEdgeInsets(top: 1.0, left: 6.0, bottom: 1.0, right: 1.0))
self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 5.0, left: 9.0, bottom: 4.0, right: 9.0))
self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 2.0)
self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
}
}
let defaultChatMessageItemLayoutConstants = ChatMessageItemLayoutConstants()
public class ChatMessageItemView: ListViewItemNode {
let layoutConstants = defaultChatMessageItemLayoutConstants
var item: ChatMessageItem?
var controllerInteraction: ChatControllerInteraction?
public required convenience init() {
self.init(layerBacked: true)
}
public init(layerBacked: Bool) {
super.init(layerBacked: layerBacked, dynamicBounce: true)
self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func reuse() {
super.reuse()
self.item = nil
self.frame = CGRect()
}
func setupItem(_ item: ChatMessageItem) {
self.item = item
}
override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? ChatMessageItem {
let doLayout = self.asyncLayout()
let merged = item.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = doLayout(item, width, merged.top, merged.bottom)
self.contentSize = layout.contentSize
self.insets = layout.insets
apply(.None)
}
}
override public func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
if let avatarNode = accessoryItemNode as? ChatMessageAvatarAccessoryItemNode {
avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: self.bounds.height - 38.0 - self.insets.top + 1.0), size: CGSize(width: 38.0, height: 38.0))
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
super.animateInsertion(currentTimestamp, duration: duration)
self.transitionOffset = -self.bounds.size.height * 1.6
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
//self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration)
}
func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
return { _, _, _, _ in
return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _ in
})
}
}
func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? {
return nil
}
func updateHiddenMedia() {
}
}

View File

@@ -0,0 +1,104 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
override var properties: ChatMessageBubbleContentProperties {
return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0)
}
private let interactiveImageNode: ChatMessageInteractiveMediaNode
private var item: ChatMessageItem?
private var media: Media?
required init() {
self.interactiveImageNode = ChatMessageInteractiveMediaNode()
super.init()
self.addSubnode(self.interactiveImageNode)
self.interactiveImageNode.activateLocalContent = { [weak self] in
if let strongSelf = self {
if let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction {
controllerInteraction.openMessage(item.message.id)
}
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let interactiveImageLayout = self.interactiveImageNode.asyncLayout()
return { item, layoutConstants, position, constrainedSize in
var selectedMedia: Media?
for media in item.message.media {
if let telegramImage = media as? TelegramMediaImage {
selectedMedia = telegramImage
} else if let telegramFile = media as? TelegramMediaFile {
selectedMedia = telegramFile
}
}
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))
return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in
let (refinedWidth, finishLayout) = refineLayout(constrainedSize)
return (refinedWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { boundingWidth in
let (imageSize, imageApply) = finishLayout(boundingWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
return (CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.media = selectedMedia
strongSelf.interactiveImageNode.frame = CGRect(origin: CGPoint(x: layoutConstants.image.bubbleInsets.left, y: layoutConstants.image.bubbleInsets.top), size: imageSize)
imageApply()
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.interactiveImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.interactiveImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func transitionNode(media: Media) -> ASDisplayNode? {
if let currentMedia = self.media, currentMedia.isEqual(media) {
return self.interactiveImageNode
}
return nil
}
override func updateHiddenMedia(_ media: [Media]?) {
var mediaHidden = false
if let currentMedia = self.media, let media = media {
for item in media {
if item.isEqual(currentMedia) {
mediaHidden = true
break
}
}
}
self.interactiveImageNode.isHidden = mediaHidden
}
}

View File

@@ -0,0 +1,96 @@
import Foundation
import AsyncDisplayKit
import Postbox
import Display
private let titleFont: UIFont = {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: 14.0, weight: UIFontWeightMedium)
} else {
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString?, 14.0, nil)
}
}()
private let textFont = Font.regular(14.0)
class ChatMessageReplyInfoNode: ASTransformLayerNode {
private let contentNode: ASDisplayNode
private let lineNode: ASDisplayNode
private var titleNode: TextNode?
private var textNode: TextNode?
override init() {
self.contentNode = ASDisplayNode()
self.contentNode.displaysAsynchronously = true
self.contentNode.isLayerBacked = true
self.contentNode.shouldRasterizeDescendants = true
self.contentNode.contentMode = .left
self.contentNode.contentsScale = UIScreenScale
self.lineNode = ASDisplayNode()
self.lineNode.displaysAsynchronously = false
self.lineNode.isLayerBacked = true
super.init()
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.lineNode)
}
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ incoming: Bool, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
return { incoming, message, constrainedSize in
let titleString = message.author?.displayTitle ?? ""
let textString = message.text
let titleColor = incoming ? UIColor(0x007bff) : UIColor(0x00a516)
let leftInset: CGFloat = 10.0
let lineColor = incoming ? UIColor(0x3ca7fe) : UIColor(0x29cc10)
let maximumTextWidth = max(0.0, constrainedSize.width - leftInset)
let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height)
let (titleLayout, titleApply) = titleNodeLayout(NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), nil, 1, .end, contrainedTextSize, nil)
let (textLayout, textApply) = textNodeLayout(NSAttributedString(string: textString, font: textFont, textColor: UIColor.black), nil, 1, .end, contrainedTextSize, nil)
let size = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + leftInset, height: titleLayout.size.height + textLayout.size.height)
return (size, {
let node: ChatMessageReplyInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageReplyInfoNode()
}
let titleNode = titleApply()
let textNode = textApply()
if node.titleNode == nil {
titleNode.isLayerBacked = true
node.titleNode = titleNode
node.contentNode.addSubnode(titleNode)
}
if node.textNode == nil {
textNode.isLayerBacked = true
node.textNode = textNode
node.contentNode.addSubnode(textNode)
}
titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: titleLayout.size)
textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleLayout.size.height), size: textLayout.size)
node.lineNode.backgroundColor = lineColor
node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 2.5), size: CGSize(width: 2.0, height: size.height - 3.0))
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
})
}
}
}

View File

@@ -0,0 +1,98 @@
import Foundation
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
class ChatMessageStickerItemNode: ChatMessageItemView {
let imageNode: TransformImageNode
var progressNode: RadialProgressNode?
var tapRecognizer: UITapGestureRecognizer?
var telegramFile: TelegramMediaFile?
private let fetchDisposable = MetaDisposable()
required init() {
self.imageNode = TransformImageNode()
super.init(layerBacked: false)
self.imageNode.displaysAsynchronously = false
self.addSubnode(self.imageNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
override func setupItem(_ item: ChatMessageItem) {
super.setupItem(item)
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
if self.telegramFile != telegramFile {
self.telegramFile = telegramFile
let signal = chatMessageSticker(account: item.account, file: telegramFile)
self.imageNode.setSignal(account: item.account, signal: signal)
self.fetchDisposable.set(fileInteractiveFetched(account: item.account, file: telegramFile).start())
}
break
}
}
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let displaySize = CGSize(width: 200.0, height: 200.0)
let telegramFile = self.telegramFile
let layoutConstants = self.layoutConstants
let imageLayout = self.imageNode.asyncLayout()
return { item, width, mergedTop, mergedBottom in
let incoming = item.account.peerId != item.message.author?.id
var imageSize: CGSize = CGSize(width: 100.0, height: 100.0)
if let telegramFile = telegramFile {
if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions {
imageSize = thumbnailSize.aspectFitted(displaySize)
}
}
let avatarInset: CGFloat = (item.peerId.isGroup && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0
let layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0)
let imageFrame = CGRect(origin: CGPoint(x: (incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - imageSize.width - layoutConstants.bubble.edgeInset)), y: 0.0), size: imageSize)
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageFrame.size, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets())
let imageApply = imageLayout(arguments)
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in
if let strongSelf = self {
strongSelf.imageNode.frame = imageFrame
strongSelf.progressNode?.position = strongSelf.imageNode.position
imageApply()
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
super.animateInsertion(currentTimestamp, duration: duration)
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}

View File

@@ -0,0 +1,170 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0)
private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0)
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let textNode: TextNode
private let statusNode: ChatMessageDateAndStatusNode
required init() {
self.textNode = TextNode()
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.textNode.isLayerBacked = true
self.textNode.contentMode = .topLeft
self.textNode.contentsScale = UIScreenScale
self.textNode.displaysAsynchronously = true
self.addSubnode(self.textNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let textLayout = TextNode.asyncLayout(self.textNode)
let statusLayout = self.statusNode.asyncLayout()
return { item, layoutConstants, position, _ in
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
let message = item.message
let incoming = item.account.peerId != message.author?.id
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
var t = Int(item.message.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
//let dateText = "\(message.id.id)"
let statusType: ChatMessageDateAndStatusType?
if case .None = position.bottom {
if incoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.contains(.Unsent) {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: true))
}
}
} else {
statusType = nil
}
var statusSize: CGSize?
var statusApply: (() -> Void)?
if let statusType = statusType {
let (size, apply) = statusLayout(dateText, statusType, textConstrainedSize)
statusSize = size
statusApply = apply
}
let attributedText: NSAttributedString
var entities: TextEntitiesMessageAttribute?
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute
break
}
}
if let entities = entities {
let string = NSMutableAttributedString(string: message.text, attributes: [NSFontAttributeName: messageFont, NSForegroundColorAttributeName: UIColor.black])
for entity in entities.entities {
switch entity.type {
case .Url:
string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
case .Bold:
string.addAttribute(NSFontAttributeName, value: messageBoldFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
default:
break
}
}
attributedText = string
} else {
attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: UIColor.black)
}
let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, nil)
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
let textSize = textLayout.size
var statusFrame: CGRect?
if let statusSize = statusSize {
var frame = CGRect(origin: CGPoint(), size: statusSize)
let trailingLineWidth = textLayout.trailingLineWidth
if textSize.width - trailingLineWidth >= statusSize.width {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height)
} else if trailingLineWidth + statusSize.width < textConstrainedSize.width {
frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height)
} else {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY)
}
statusFrame = frame
}
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var boundingSize: CGSize
if let statusFrame = statusFrame {
boundingSize = textFrame.union(statusFrame).size
} else {
boundingSize = textFrame.size
}
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
return (boundingSize.width, { boundingWidth in
var adjustedStatusFrame: CGRect?
if let statusFrame = statusFrame {
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: statusFrame.origin.y), size: statusFrame.size)
}
return (boundingSize, { [weak self] in
if let strongSelf = self {
let _ = textApply()
if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame {
strongSelf.statusNode.frame = adjustedStatusFrame
statusApply()
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
strongSelf.textNode.frame = textFrame
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}

View File

@@ -0,0 +1,392 @@
import Foundation
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
private func generateLineImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 2.0, height: 3.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 1.0), size: CGSize(width: 2.0, height: 2.0)))
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)
}
private let incomingLineImage = generateLineImage(color: UIColor(0x3ca7fe))
private let outgoingLineImage = generateLineImage(color: UIColor(0x29cc10))
private let incomingAccentColor = UIColor(0x3ca7fe)
private let outgoingAccentColor = UIColor(0x00a700)
private let titleFont: UIFont = UIFont.boldSystemFont(ofSize: 15.0)
private let textFont: UIFont = UIFont.systemFont(ofSize: 15.0)
final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
private let lineNode: ASImageNode
private let textNode: TextNode
private let inlineImageNode: TransformImageNode
private var contentImageNode: ChatMessageInteractiveMediaNode?
private var contentFileNode: ChatMessageInteractiveFileNode?
private let statusNode: ChatMessageDateAndStatusNode
private var image: TelegramMediaImage?
required init() {
self.lineNode = ASImageNode()
self.lineNode.isLayerBacked = true
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.textNode = TextNode()
self.textNode.isLayerBacked = true
self.textNode.displaysAsynchronously = true
self.textNode.contentsScale = UIScreenScale
self.textNode.contentMode = .topLeft
self.inlineImageNode = TransformImageNode()
self.inlineImageNode.isLayerBacked = true
self.inlineImageNode.displaysAsynchronously = false
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.addSubnode(self.lineNode)
self.addSubnode(self.textNode)
self.addSubnode(self.statusNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) {
let textAsyncLayout = TextNode.asyncLayout(self.textNode)
let currentImage = self.image
let imageLayout = self.inlineImageNode.asyncLayout()
let statusLayout = self.statusNode.asyncLayout()
let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode)
let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode)
return { item, layoutConstants, _, constrainedSize in
let insets = UIEdgeInsets(top: 0.0, left: 9.0 + 8.0, bottom: 5.0, right: 8.0)
var webpage: TelegramMediaWebpageLoadedContent?
for media in item.message.media {
if let media = media as? TelegramMediaWebpage {
if case let .Loaded(content) = media.content {
webpage = content
}
break
}
}
var textString: NSAttributedString?
var inlineImageDimensions: CGSize?
var inlineImageSize: CGSize?
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext, NoError>?
var textCutout: TextNodeCutout?
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
var refineContentImageLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode)))?
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))?
if let webpage = webpage {
let string = NSMutableAttributedString()
var notEmpty = false
if let websiteName = webpage.websiteName, !websiteName.isEmpty {
string.append(NSAttributedString(string: websiteName, font: titleFont, textColor: item.message.flags.contains(.Incoming) ? incomingAccentColor : outgoingAccentColor))
notEmpty = true
}
if let title = webpage.title, !title.isEmpty {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: UIColor.black))
}
string.append(NSAttributedString(string: title, font: titleFont, textColor: UIColor.black))
notEmpty = true
}
if let text = webpage.text, !text.isEmpty {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: UIColor.black))
}
string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: UIColor.black))
notEmpty = true
}
textString = string
if let file = webpage.file {
if file.isVideo {
let (initialImageWidth, refineLayout) = contentImageLayout(item.account, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height))
initialWidth = initialImageWidth + insets.left + insets.right
refineContentImageLayout = refineLayout
} else {
let (_, refineLayout) = contentFileLayout(item.account, file, item.message.flags.contains(.Incoming), CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height))
refineContentFileLayout = refineLayout
}
} else if let image = webpage.image {
if let type = webpage.type, ["photo"].contains(type) {
let (initialImageWidth, refineLayout) = contentImageLayout(item.account, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height))
initialWidth = initialImageWidth + insets.left + insets.right
refineContentImageLayout = refineLayout
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
inlineImageDimensions = dimensions
if image != currentImage {
updateInlineImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: image)
}
}
}
}
if let _ = inlineImageDimensions {
inlineImageSize = CGSize(width: 54.0, height: 54.0)
if let inlineImageSize = inlineImageSize {
textCutout = TextNodeCutout(position: .TopRight, size: CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0))
}
}
return (initialWidth, { constrainedSize in
var t = Int(item.message.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
//let dateText = "\(message.id.id)"
let statusType: ChatMessageDateAndStatusType
if item.message.flags.contains(.Incoming) {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if item.message.flags.contains(.Unsent) {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: true))
}
}
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
var statusSizeAndApply: (CGSize, () -> Void)?
if refineContentImageLayout == nil && refineContentFileLayout == nil {
statusSizeAndApply = statusLayout(dateText, statusType, textConstrainedSize)
}
let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, textCutout)
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
var statusFrame: CGRect?
if let (statusSize, _) = statusSizeAndApply {
var frame = CGRect(origin: CGPoint(), size: statusSize)
let trailingLineWidth = textLayout.trailingLineWidth
if textLayout.size.width - trailingLineWidth >= statusSize.width {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height)
} else if trailingLineWidth + statusSize.width < textConstrainedSize.width {
frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height)
} else {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY)
}
if let inlineImageSize = inlineImageSize {
if frame.origin.y < inlineImageSize.height + 4.0 {
frame.origin.y = inlineImageSize.height + 4.0
}
}
frame = frame.offsetBy(dx: insets.left, dy: insets.top)
statusFrame = frame
}
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
let lineImage = item.message.flags.contains(.Incoming) ? incomingLineImage : outgoingLineImage
var boundingSize = textFrame.size
if let statusFrame = statusFrame {
boundingSize = textFrame.union(statusFrame).size
}
var lineHeight = textFrame.size.height
if let inlineImageSize = inlineImageSize {
if boundingSize.height < inlineImageSize.height {
boundingSize.height = inlineImageSize.height
}
if lineHeight < inlineImageSize.height {
lineHeight = inlineImageSize.height
}
}
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))?
if let refineContentImageLayout = refineContentImageLayout {
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize)
finalizeContentImageLayout = finalizeImageLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))?
if let refineContentFileLayout = refineContentFileLayout {
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
finalizeContentFileLayout = finalizeFileLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
boundingSize.width += insets.left + insets.right
boundingSize.height += insets.top + insets.bottom
lineHeight += insets.top + insets.bottom
var imageApply: (() -> Void)?
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets())
imageApply = imageLayout(arguments)
}
return (boundingSize.width, { boundingWidth in
var adjustedBoundingSize = boundingSize
var adjustedLineHeight = lineHeight
var imageFrame: CGRect?
if let inlineImageSize = inlineImageSize {
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize)
}
var contentImageSizeAndApply: (CGSize, () -> ChatMessageInteractiveMediaNode)?
if let finalizeContentImageLayout = finalizeContentImageLayout {
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
contentImageSizeAndApply = (size, apply)
var imageHeigthAddition = size.height
if textFrame.size.height > CGFloat(FLT_EPSILON) {
imageHeigthAddition += 2.0
}
adjustedBoundingSize.height += imageHeigthAddition + 5.0
adjustedLineHeight += imageHeigthAddition + 4.0
}
var contentFileSizeAndApply: (CGSize, () -> ChatMessageInteractiveFileNode)?
if let finalizeContentFileLayout = finalizeContentFileLayout {
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
contentFileSizeAndApply = (size, apply)
var imageHeigthAddition = size.height
if textFrame.size.height > CGFloat(FLT_EPSILON) {
imageHeigthAddition += 2.0
}
adjustedBoundingSize.height += imageHeigthAddition + 5.0
adjustedLineHeight += imageHeigthAddition + 4.0
}
var adjustedStatusFrame: CGRect?
if let statusFrame = statusFrame {
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size)
}
return (adjustedBoundingSize, { [weak self] in
if let strongSelf = self {
strongSelf.lineNode.image = lineImage
strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 0.0), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0))
let _ = textApply()
strongSelf.textNode.frame = textFrame
if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame {
strongSelf.statusNode.frame = adjustedStatusFrame
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
}
statusApply()
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
strongSelf.image = webpage?.image
if let imageFrame = imageFrame {
if let updateImageSignal = updateInlineImageSignal {
strongSelf.inlineImageNode.setSignal(account: item.account, signal: updateImageSignal)
}
strongSelf.inlineImageNode.frame = imageFrame
if strongSelf.inlineImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.inlineImageNode)
}
if let imageApply = imageApply {
imageApply()
}
} else if strongSelf.inlineImageNode.supernode != nil {
strongSelf.inlineImageNode.removeFromSupernode()
}
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
let contentImageNode = contentImageApply()
if strongSelf.contentImageNode !== contentImageNode {
strongSelf.contentImageNode = contentImageNode
strongSelf.addSubnode(contentImageNode)
contentImageNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.controllerInteraction?.openMessage(item.message.id)
}
}
}
let _ = contentImageApply()
contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat(FLT_EPSILON) ? 4.0 : 0.0)), size: contentImageSize)
} else if let contentImageNode = strongSelf.contentImageNode {
contentImageNode.removeFromSupernode()
strongSelf.contentImageNode = nil
}
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
let contentFileNode = contentFileApply()
if strongSelf.contentFileNode !== contentFileNode {
strongSelf.contentFileNode = contentFileNode
strongSelf.addSubnode(contentFileNode)
contentFileNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.controllerInteraction?.openMessage(item.message.id)
}
}
}
let _ = contentFileApply()
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat(FLT_EPSILON) ? 4.0 : 0.0)), size: contentFileSize)
} else if let contentFileNode = strongSelf.contentFileNode {
contentFileNode.removeFromSupernode()
strongSelf.contentFileNode = nil
}
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateInsertionIntoBubble(_ duration: Double) {
self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}

View File

@@ -0,0 +1,95 @@
import Foundation
import UIKit
import Postbox
import AsyncDisplayKit
import Display
private func backgroundImage() -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 25.0), contextGenerator: { size, context -> Void in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(white: 0.0, alpha: 0.2).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
context.setFillColor(UIColor(white: 1.0, alpha: 0.9).cgColor)
context.fill(CGRect(x: 0.0, y: UIScreenPixel, width: size.width, height: size.height - UIScreenPixel - UIScreenPixel))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
}
private let titleFont = UIFont.systemFont(ofSize: 13.0)
class ChatUnreadItem: ListViewItem {
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
async {
let node = ChatUnreadItemNode()
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
completion(node, {})
}
}
}
class ChatUnreadItemNode: ListViewItemNode {
let backgroundNode: ASImageNode
let labelNode: TextNode
init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.labelNode = TextNode()
self.labelNode.isLayerBacked = true
super.init(layerBacked: true)
self.backgroundNode.image = backgroundImage()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.labelNode)
self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0)
self.scrollPositioningInsets = UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 0.0)
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
super.animateInsertion(currentTimestamp, duration: duration)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
//self.transitionOffset = -self.bounds.size.height * 1.6
//self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
//self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let (layout, apply) = self.asyncLayout()(width)
apply()
self.contentSize = layout.contentSize
self.insets = layout.insets
}
func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
return { width in
let (size, apply) = labelLayout(NSAttributedString(string: "Unread", font: titleFont, textColor: UIColor(0x86868d)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil)
let backgroundSize = CGSize(width: width, height: 25.0)
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 25.0), insets: UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in
if let strongSelf = self {
let _ = apply()
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size)
}
})
}
}
}

View File

@@ -0,0 +1,230 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
class ChatVideoGalleryItem: GalleryItem {
let account: Account
let message: Message
let location: MessageHistoryEntryLocation?
init(account: Account, message: Message, location: MessageHistoryEntryLocation?) {
self.account = account
self.message = message
self.location = location
}
func node() -> GalleryItemNode {
let node = ChatVideoGalleryItemNode()
for media in self.message.media {
if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) {
node.setFile(account: account, file: file)
break
}
}
if let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
return node
}
func updateNode(node: GalleryItemNode) {
if let node = node as? ChatVideoGalleryItemNode, let location = self.location {
node._title.set(.single("\(location.index + 1) of \(location.count)"))
}
}
}
final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode {
fileprivate let _ready = Promise<Void>()
fileprivate let _title = Promise<String>()
fileprivate let _titleView = Promise<UIView?>()
private var player: MediaPlayer?
private let snapshotNode: TransformImageNode
private let videoNode: MediaPlayerNode
private let scrubberView: ChatVideoGalleryItemScrubberView
private var accountAndFile: (Account, TelegramMediaFile)?
private var isCentral = false
private let videoStatusDisposable = MetaDisposable()
override init() {
self.videoNode = MediaPlayerNode()
self.snapshotNode = TransformImageNode()
self.snapshotNode.backgroundColor = UIColor.black
self.videoNode.snapshotNode = snapshotNode
self.scrubberView = ChatVideoGalleryItemScrubberView()
super.init()
self.snapshotNode.imageUpdated = { [weak self] in
self?._ready.set(.single(Void()))
}
self._titleView.set(.single(self.scrubberView))
self.scrubberView.seek = { [weak self] timestamp in
self?.player?.seek(timestamp: timestamp)
}
}
deinit {
self.videoStatusDisposable.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
func setFile(account: Account, file: TelegramMediaFile) {
if self.accountAndFile == nil || !self.accountAndFile!.1.isEqual(file) {
if let largestSize = file.dimensions {
self.snapshotNode.alphaTransitionOnFirstUpdate = false
let displaySize = largestSize.dividedByScreenScale()
self.snapshotNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
self.snapshotNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: true), dispatchOnDisplayLink: false)
self.zoomableContent = (largestSize, self.videoNode)
} else {
self._ready.set(.single(Void()))
}
let shouldPlayVideo = self.accountAndFile?.1 != file
self.accountAndFile = (account, file)
if shouldPlayVideo && self.isCentral {
self.playVideo()
}
}
}
private func playVideo() {
if let (account, file) = self.accountAndFile {
var dimensions: CGSize? = file.dimensions
if dimensions == nil || dimensions!.width.isLessThanOrEqualTo(0.0) || dimensions!.height.isLessThanOrEqualTo(0.0) {
dimensions = largestImageRepresentation(file.previewRepresentations)?.dimensions.aspectFitted(CGSize(width: 1920, height: 1080))
}
if dimensions == nil || dimensions!.width.isLessThanOrEqualTo(0.0) || dimensions!.height.isLessThanOrEqualTo(0.0) {
dimensions = CGSize(width: 1920, height: 1080)
}
if let dimensions = dimensions, !dimensions.width.isLessThanOrEqualTo(0.0) && !dimensions.height.isLessThanOrEqualTo(0.0) {
/*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size))
self.videoNode.player = VideoPlayer(source: source)*/
let player = MediaPlayer(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size))
player.attachPlayerNode(self.videoNode)
self.player = player
self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.scrubberView.setStatus(status)
}
}))
player.play()
self.zoomableContent = (dimensions, self.videoNode)
}
}
}
private func stopVideo() {
self.player = nil
}
override func centralityUpdated(isCentral: Bool) {
super.centralityUpdated(isCentral: isCentral)
if self.isCentral != isCentral {
self.isCentral = isCentral
if isCentral {
self.playVideo()
} else {
self.stopVideo()
}
}
}
override func animateIn(from node: ASDisplayNode) {
var transformedFrame = node.view.convert(node.view.bounds, to: self.videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.videoNode.view.superview)
self.videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.videoNode.layer.transform, transformedFrame.size.width / self.videoNode.layer.bounds.size.width, transformedFrame.size.height / self.videoNode.layer.bounds.size.height, 1.0)
self.videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
}
override func animateOut(to node: ASDisplayNode, completion: @escaping () -> Void) {
var transformedFrame = node.view.convert(node.view.bounds, to: self.videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.videoNode.view.superview)
let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.videoNode.view.convert(self.videoNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let copyView = node.view.snapshotContentTree()!
self.view.insertSubview(copyView, belowSubview: self.scrollView)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
self.videoNode.layer.animatePosition(from: self.videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.videoNode.snapshotNode?.isHidden = true
transformedFrame.origin = CGPoint()
/*self.videoNode.layer.animateBounds(from: self.videoNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})*/
let transform = CATransform3DScale(self.videoNode.layer.transform, transformedFrame.size.width / self.videoNode.layer.bounds.size.width, transformedFrame.size.height / self.videoNode.layer.bounds.size.height, 1.0)
self.videoNode.layer.animate(from: NSValue(caTransform3D: self.videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
}
override func title() -> Signal<String, NoError> {
//return self._title.get()
return .single("")
}
override func titleView() -> Signal<UIView?, NoError> {
return self._titleView.get()
}
}

View File

@@ -0,0 +1,93 @@
import Foundation
import UIKit
final class ChatVideoGalleryItemScrubberView: UIView {
private let backgroundView: UIView
private let foregroundView: UIView
private let handleView: UIView
private var status: MediaPlayerStatus?
private var scrubbing = false
private var scrubbingLocation: CGFloat = 0.0
private var initialScrubbingPosition: CGFloat = 0.0
private var scrubbingPosition: CGFloat = 0.0
var seek: (Double) -> Void = { _ in }
override init(frame: CGRect) {
self.backgroundView = UIView()
self.backgroundView.backgroundColor = UIColor.gray
self.backgroundView.clipsToBounds = true
self.foregroundView = UIView()
self.foregroundView.backgroundColor = UIColor.white
self.handleView = UIView()
self.handleView.backgroundColor = UIColor.white
super.init(frame: frame)
self.backgroundView.addSubview(self.foregroundView)
self.addSubview(self.backgroundView)
self.addSubview(self.handleView)
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setStatus(_ status: MediaPlayerStatus) {
self.status = status
self.layoutSubviews()
if status.status == .playing {
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let status = self.status, status.duration > 0.0 else {
return
}
switch recognizer.state {
case .began:
self.scrubbing = true
self.scrubbingLocation = recognizer.location(in: self).x
self.initialScrubbingPosition = CGFloat(status.timestamp / status.duration)
self.scrubbingPosition = 0.0
case .changed:
let distance = recognizer.location(in: self).x - self.scrubbingLocation
self.scrubbingPosition = self.initialScrubbingPosition + (distance / self.bounds.size.width)
self.layoutSubviews()
case .ended:
self.scrubbing = false
self.seek(Double(self.scrubbingPosition) * status.duration)
default:
break
}
}
override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
let barHeight: CGFloat = 2.0
let handleHeight: CGFloat = 14.0
self.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor(size.height - barHeight) / 2.0), size: CGSize(width: size.width, height: barHeight))
var position: CGFloat = 0.0
if self.scrubbing {
position = self.scrubbingPosition
} else {
if let status = self.status, status.duration > 0.0 {
position = CGFloat(status.timestamp / status.duration)
}
}
self.foregroundView.frame = CGRect(origin: CGPoint(x: -size.width + floor(position * size.width), y: 0.0), size: CGSize(width: size.width, height: barHeight))
self.handleView.frame = CGRect(origin: CGPoint(x: floor(position * size.width), y: floor(size.height - handleHeight) / 2.0), size: CGSize(width: 1.5, height: handleHeight))
}
}

View File

@@ -0,0 +1,2 @@
SWIFT_INCLUDE_PATHS = $(SRCROOT)/TelegramUI
MODULEMAP_PRIVATE_FILE = $(SRCROOT)/TelegramUI/module.private.modulemap

Some files were not shown because too many files have changed in this diff Show More