no message
9
Images.xcassets/Chat List/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
9
Images.xcassets/Chat List/Tabs/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
21
Images.xcassets/Chat List/Tabs/IconChats.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconChats.imageset/TabIconMessages@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
21
Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconChatsSelected.imageset/TabIconMessages_Highlighted@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 877 B |
21
Images.xcassets/Chat List/Tabs/IconContacts.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconContacts.imageset/TabIconContacts@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1011 B |
21
Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconContactsSelected.imageset/TabIconContacts_Highlighted@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 845 B |
21
Images.xcassets/Chat List/Tabs/IconSettings.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconSettings.imageset/TabIconSettings@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
21
Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat List/Tabs/IconSettingsSelected.imageset/TabIconSettings_Highlighted@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
9
Images.xcassets/Chat/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
9
Images.xcassets/Chat/Input/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
9
Images.xcassets/Chat/Input/Text/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
12
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IconAttachment.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/IconAttachment.pdf
vendored
Normal file
35
Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/ModernBubbleIncomingFullPad@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/ModernBubbleOutgoingFullPad@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
35
Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
9
Images.xcassets/Chat/Message/Background/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
9
Images.xcassets/Chat/Message/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
22
Images.xcassets/Chat/Message/RadialProgressIconDocumentIncoming.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
22
Images.xcassets/Chat/Message/RadialProgressIconDocumentOutgoing.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
21
Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Wallpapers/Builtin0.imageset/builtin-wallpaper-0.jpg
vendored
Normal file
|
After Width: | Height: | Size: 168 KiB |
9
Images.xcassets/Chat/Wallpapers/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<key>TelegramUI.xcscheme</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
||||
69
TelegramUI/ActionSheetRollImageItem.swift
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
TelegramUI/AlertController.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
class AlertController {
|
||||
|
||||
}
|
||||
84
TelegramUI/AuthorizationCodeController.swift
Normal 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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
21
TelegramUI/AuthorizationCodeControllerNode.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
84
TelegramUI/AuthorizationController.swift
Normal 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 {
|
||||
}
|
||||
}
|
||||
57
TelegramUI/AuthorizationPasswordController.swift
Normal 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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
21
TelegramUI/AuthorizationPasswordControllerNode.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
75
TelegramUI/AuthorizationPhoneController.swift
Normal 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))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
21
TelegramUI/AuthorizationPhoneControllerNode.swift
Normal 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
@@ -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
|
||||
}
|
||||
}
|
||||
846
TelegramUI/ChatController.swift
Normal 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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
14
TelegramUI/ChatControllerInteraction.swift
Normal 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
|
||||
}
|
||||
}
|
||||
183
TelegramUI/ChatControllerNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
138
TelegramUI/ChatDocumentGalleryItem.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
74
TelegramUI/ChatHistoryEntry.swift
Normal 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
|
||||
}
|
||||
}
|
||||
23
TelegramUI/ChatHistoryLocation.swift
Normal 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
|
||||
}
|
||||
}
|
||||
52
TelegramUI/ChatHistoryNavigationButtonNode.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
21
TelegramUI/ChatHoleGalleryItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
75
TelegramUI/ChatHoleItem.swift
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
214
TelegramUI/ChatImageGalleryItem.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
199
TelegramUI/ChatInputView.swift
Normal 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)
|
||||
}*/
|
||||
}
|
||||
177
TelegramUI/ChatListAvatarNode.swift
Normal 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)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
499
TelegramUI/ChatListController.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
TelegramUI/ChatListControllerNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
47
TelegramUI/ChatListEmptyItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
105
TelegramUI/ChatListHoleItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
409
TelegramUI/ChatListItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
126
TelegramUI/ChatListSearchContainerNode.swift
Normal 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 })
|
||||
}
|
||||
}
|
||||
101
TelegramUI/ChatListSearchItem.swift
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
57
TelegramUI/ChatListSearchRecentPeersNode.swift
Normal 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 })
|
||||
}
|
||||
}
|
||||
50
TelegramUI/ChatMediaActionSheetController.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
104
TelegramUI/ChatMediaActionSheetRollItem.swift
Normal 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()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
133
TelegramUI/ChatMessageActionItemNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
54
TelegramUI/ChatMessageAvatarAccessoryItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
63
TelegramUI/ChatMessageBubbleContentNode.swift
Normal 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]?) {
|
||||
}
|
||||
}
|
||||
660
TelegramUI/ChatMessageBubbleItemNode.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
TelegramUI/ChatMessageDateAndStatusNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
73
TelegramUI/ChatMessageFileBubbleContentNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
53
TelegramUI/ChatMessageForwardInfoNode.swift
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
247
TelegramUI/ChatMessageInteractiveFileNode.swift
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
260
TelegramUI/ChatMessageInteractiveMediaNode.swift
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
TelegramUI/ChatMessageItem.swift
Normal 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)\")"
|
||||
}
|
||||
}
|
||||
120
TelegramUI/ChatMessageItemView.swift
Normal 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() {
|
||||
}
|
||||
}
|
||||
104
TelegramUI/ChatMessageMediaBubbleContentNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
96
TelegramUI/ChatMessageReplyInfoNode.swift
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
98
TelegramUI/ChatMessageStickerItemNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
170
TelegramUI/ChatMessageTextBubbleContentNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
392
TelegramUI/ChatMessageWebpageBubbleContentNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
95
TelegramUI/ChatUnreadItem.swift
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
230
TelegramUI/ChatVideoGalleryItem.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
93
TelegramUI/ChatVideoGalleryItemScrubberView.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
2
TelegramUI/Config/TelegramUI.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
SWIFT_INCLUDE_PATHS = $(SRCROOT)/TelegramUI
|
||||
MODULEMAP_PRIVATE_FILE = $(SRCROOT)/TelegramUI/module.private.modulemap
|
||||